Next.jsをやってみる
初期化:
bunx create-next-app
# or
bun create next-app
参考:
言語は@/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>
);
}
/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>
);
}
Next.jsでは、a
の代わりにLink
を使う
import Link from "next/link";
export default function Component() {
return <Link href="/about">About</Link>
}
ルーティングはsrc/app/
に置く。
フォルダ名がそのままパスになるらしい。
page.tsx
でデフォルトエクスポートしたものがレンダリングされる。
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>
)
}
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>
)
}
APIのフェッチが終わるまでレスポンスを返すのを待てるNext.js(SSR?)の特権かな、これ
クライアントサイドしか触れないReactではできなそう
404ページを表示する時はnext/navigation
のnotFound
が使える。
めっちゃ簡単に404ページに飛ばせる...
import { notFound } from "next/navigation";
export default function Page({ params }: Props) {
// 略
if (!data) {
return notFound()
}
return // 略
}
Next.jsでAPIを作るには、src/app
にapi
フォルダを作って、その中に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 }
ページタイトルは、page.tsx
でmetadata
をexport
することで設定できる。
import type { Metadata } from "next";
export const metadata: Metadata = {
title: 'ホーム | はじめてのNext.js'
}
動的に設定する場合は、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。
参考:
もしかして、SSRになるかSSGになるかって具体的なコード依存...?
キャッシュすればSSGで、キャッシュしなければSSRみたいな感じ?
動的にパスパラメータを設定する場合、基本的にはSSRになるっぽい。
リクエストのたびにページを再生成するみたい。
動的なルーティングをビルド時に静的に決定したい場合は、いくつかの設定が必要。
-
dynamicParams
をfalse
に設定 -
generateStaticParams
関数をエクスポート
generateStaticParams
はApp Router用の関数で、パスパラメータを静的に決定するための関数。
この関数が返した値がパラメータになる。
例えば/blogs/[id]
というルーティングがあった場合、以下のようにすることで、1
と2
という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をフェッチしていたので、これでパフォーマンスの改善が見込めると思われる。多分。
参考:
おまけ:Honoと統合する
Next.jsのAPIでHonoを使うには、Honoが用意しているミドルウェア?を使う。
手順は以下。
-
app/api/[[...route]]/route.ts
を作成する -
hono/vercel
からhandle
をインポートする -
Hono
のインスタンスをhandle
で包み、それをGET
やPOST
としてエクスポートする
なお、app/api/[[...route]]/route.ts
はそのままコピペする。
[[...route]]
という記法はOptional Catch-all Segumentsというものらしい。
例えばこんな感じ(ドキュメントの丸パクリ)
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)
なお、コードはHonoなのでNext.js外で動かすことも可能。
その場合は少し工夫が必要になる。
- トップレベル(?)の
Hono
インスタンスをdefault export
するファイルを作る -
app/api/[[...route]]/route.ts
で1をインポートし、Next.js用に変換してエクスポートする -
package.json
に1のファイルを動かすコマンドを追記する
1と2を分けているのは、略/route.ts
でdefault export
するものがあると、このファイルでメソッド以外のものがエクスポートされているとNext.jsに怒られるから。
動きはするので、最悪同じファイルでもいいかも?
例(Bunを使用)
ファイル分けを以下のように工夫する。
// ルーティングを記述
const app = new Hono()
.get('/', c => c.json({ message: 'Hello World!' }))
// この辺は各ランタイムで変わる
export default app
// Next.js用
import app from "./app"
import { handle } from "hono/vercel"
export const GET = handle(app)
そして、コマンドを追記する。
これはHonoを通常の使い方で使う時と同じものを書く。
{
// ...
"scripts": {
// ...
"api": "bun run --hot app/api/[[...route]]/app.ts"
}
// ...
}
これにより、Next.jsのサーバーを立ち上げるとその中にAPIが含まれることになるが、それと同じAPIを単体でも動かせるようになる。
つまりフロントエンド&バックエンドで動かすこともできるが、バックエンド単体で動かすこともできる。
参考