Closed23

Next.jsをやってみる

nanasinanasi

言語は@/src/app/layout.tsxで変えられる

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja"> { /* ここ */ }
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        {children}
      </body>
    </html>
  );
}
nanasinanasi

/src/app/layout.tsxは全体に適用されるコンポーネントを書く。
例えばヘッダーなんかを置く。

/src/app/layout.tsx
import Header from "@/components/Header";

// 略

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body>
        <Header />
        <main>{children}</main>
      </body>
    </html>
  );
}
nanasinanasi

Next.jsでは、aの代わりにLinkを使う

import Link from "next/link";

export default function Component() {
    return <Link href="/about">About</Link>
}
nanasinanasi

ルーティングはsrc/app/に置く。
フォルダ名がそのままパスになるらしい。
page.tsxでデフォルトエクスポートしたものがレンダリングされる。

nanasinanasi

useStateなどのフックを使う時は、use clientでクライアントコンポーネントにする必要がある。
クライアントコンポーネントはクライアントでレンダリングされる。
基本的にはサーバーサイドでレンダリングされるサーバーコンポーネントを使うべきっぽい?
で、必要になった時だけuse clientを使う。

"use client";
import { useState } from "react"

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(c => c - 1)}>-</button>
    </div>
  )
}
nanasinanasi

Next.jsでAPIをフェッチする時は、いちいちクライアントサイドでuseEffectしなくてもいいみたい。
asyncがついたコンポーネントをそのままdefault exportできる。

export default async function Page() {
  const res = await fetch('http://localhost:3000/api/blog')
    .then(res => res.json())

  return (
    <p>{res.body}</p>
  )
}

nanasinanasi

APIのフェッチが終わるまでレスポンスを返すのを待てるNext.js(SSR?)の特権かな、これ
クライアントサイドしか触れないReactではできなそう

nanasinanasi

404ページを表示する時はnext/navigationnotFoundが使える。
めっちゃ簡単に404ページに飛ばせる...

import { notFound } from "next/navigation";

export default function Page({ params }: Props) {
  // 略

  if (!data) {
    return notFound()
  }

  return // 略
}
nanasinanasi

Next.jsでAPIを作るには、src/appapiフォルダを作って、その中にroute.tsを置く。
例えば/api/blogというAPIを作るなら、src/app/api/blog/route.tsを作る。

GETなどのメソッドを名前にした関数がハンドラーとなる。
ハンドラーはNextResponse.jsonなどを返す。

import { NextResponse } from "next/server";

const GET = () => {
  return NextResponse.json({body: 'Hello World!'})
}

export { GET }
Hidden comment
nanasinanasi

ページタイトルは、page.tsxmetadataexportすることで設定できる。

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: 'ホーム | はじめてのNext.js'
}
nanasinanasi

動的に設定する場合は、generateMetadata関数をexportする。
この関数はMetadataを返す。

export async function generateMetadata({ params }: Props) {
  const res = await fetch(/* 略 */).then(res => res.json())
  return {
    title: `${res.title} | はじめてのNext.js`
  }
}

このように、第一引数にページコンポーネントと同じ(多分)パラメーターを取る。
Promiseを返してもOK。

参考:
https://zenn.dev/jinku/articles/7ba3b1220f0815

nanasinanasi

もしかして、SSRになるかSSGになるかって具体的なコード依存...?
キャッシュすればSSGで、キャッシュしなければSSRみたいな感じ?

nanasinanasi

動的にパスパラメータを設定する場合、基本的にはSSRになるっぽい。
リクエストのたびにページを再生成するみたい。

動的なルーティングをビルド時に静的に決定したい場合は、いくつかの設定が必要。

  • dynamicParamsfalseに設定
  • generateStaticParams関数をエクスポート

generateStaticParamsはApp Router用の関数で、パスパラメータを静的に決定するための関数。
この関数が返した値がパラメータになる。

例えば/blogs/[id]というルーティングがあった場合、以下のようにすることで、12というIDのブログがあることをNext.jsに伝えられる。

export const dynamicParams = false

export async function generateStaticParams(): Promise<Props['params'][]> {
  return [{ id: 1 }, { id: 2 }]
}

APIをフェッチしてidを決める場合はこうなる。(ほぼこれと同じ)

export const dynamicParams = false

export async function generateStaticParams(): Promise<Props['params'][]> {
  const blogInfo = await fetchAllBlogs() // { id: number, title: string }[]
  return blogInfo.map(({id}) => ({ id: `${id}` }))
}

こうすることで、/blog/1/blog/2というページがビルド時に生成(SSG)される。
デフォルトだとリクエストのたびにAPIをフェッチしていたので、これでパフォーマンスの改善が見込めると思われる。多分。

参考:
https://toach.biz/blog/how-to-use-generate-static-params-in-nextjs/

nanasinanasi

ちなみに、このときブログ一覧のAPIをNext.jsで作っていると、ビルド時にエラーが出る。
内容はAPIのリクエストに失敗したと言うもので、当然APIサーバーが起動していないのが理由。

無理矢理だが、開発サーバーを立てながらビルドすることで回避できた。

ターミナル1
bun run dev
ターミナル2
bun run build
nanasinanasi

あれ...?なくてもいけるようになったんだけどなんでだ...?

nanasinanasi

おまけ:Honoと統合する

Next.jsのAPIでHonoを使うには、Honoが用意しているミドルウェア?を使う。
https://hono.dev/docs/getting-started/vercel

手順は以下。

  1. app/api/[[...route]]/route.tsを作成する
  2. hono/vercelからhandleをインポートする
  3. Honoのインスタンスをhandleで包み、それをGETPOSTとしてエクスポートする

なお、app/api/[[...route]]/route.tsはそのままコピペする。
[[...route]]という記法はOptional Catch-all Segumentsというものらしい。

例えばこんな感じ(ドキュメントの丸パクリ)

app/api/[[...routes]]/route.ts
import { Hono } from 'hono'
import { handle } from 'hono/vercel'

export const runtime = 'edge'

const app = new Hono().basePath('/api')

app.get('/hello', (c) => {
  return c.json({
    message: 'Hello Next.js!',
  })
})

export const GET = handle(app)
Hidden comment
nanasinanasi

なお、コードはHonoなのでNext.js外で動かすことも可能。
その場合は少し工夫が必要になる。

  1. トップレベル(?)のHonoインスタンスをdefault exportするファイルを作る
  2. app/api/[[...route]]/route.tsで1をインポートし、Next.js用に変換してエクスポートする
  3. package.jsonに1のファイルを動かすコマンドを追記する

1と2を分けているのは、略/route.tsdefault exportするものがあると、このファイルでメソッド以外のものがエクスポートされているとNext.jsに怒られるから。
動きはするので、最悪同じファイルでもいいかも?

例(Bunを使用)

ファイル分けを以下のように工夫する。

app/api/[[...route]]/app.ts
// ルーティングを記述
const app = new Hono()
  .get('/', c => c.json({ message: 'Hello World!' }))

// この辺は各ランタイムで変わる
export default app
app/api/[[...route]]/route.ts
// Next.js用
import app from "./app"
import { handle } from "hono/vercel"

export const GET = handle(app)

そして、コマンドを追記する。
これはHonoを通常の使い方で使う時と同じものを書く。

package.json
{
  // ...
  "scripts": {
    // ...
    "api": "bun run --hot app/api/[[...route]]/app.ts"
  }
  // ...
}

これにより、Next.jsのサーバーを立ち上げるとその中にAPIが含まれることになるが、それと同じAPIを単体でも動かせるようになる。
つまりフロントエンド&バックエンドで動かすこともできるが、バックエンド単体で動かすこともできる。

このスクラップは2ヶ月前にクローズされました