Next.js App Router (app ディレクトリ) の逆引き辞典
Next.js v13 から App Router 機能 (app
ディレクトリ) が新しく追加されました。 (v13.3.0 現在はベータ版です。 v13.4.0 をもって安定版になりました!)
- ファイルベースの Layout 機能
- 処理の一部を Server Component に移しバンドルサイズを削減できる
- 例: remark を利用した Markdown のパース
が有名なところだと思いますが、アーキテクチャの大幅な変更のおかげで、 開発者とユーザー両者の体験を改善できるっぽいです。
ドキュメントが英語で書かれていて、かつ把握し切れないほど膨大なので、
pages
で使えたあの機能って、App Router ではどう書けばいいの?
という初歩的な疑問点や、ドキュメント未記載の機能に着目して、逆引きっぽい形式でざっくりとまとめてみました。
割愛した要素
以下の要素については、煩雑になる or ドキュメントだけで理解しやすそうなので、割愛しています。
- ファイルの規約 (前提知識)
-
layout.tsx
,page.tsx
など -
_someDirectory/
パターンだけは解説します
-
- Server Actions (Alpha)
- Metadata 関連
generateStaticParams
- Revalidating (ISR: Incremental Static Regeneration)
- Advanced Routing Patterns
- Parallel Routes (並行ルート)
- Intercepting Routes (横取りルート)
- Conditional Routes (条件つきルート) ... 未実装
-
Route Handlers
-
pages/api/
に相当する機能
-
-
Deduping
- Automatic
fetch()
Request Deduping cache()
- Automatic
- Streaming や preloading について
- その他
▼ Deduping についてはこちらの記事をご覧ください
前提: Routing Fundamentals
Routing Fundamentals にはかなり情報がまとまっているので、まずはそちらに目を通しましょう。
特に、上記の記事に登場する用語・ファイル名規約の一部は、この記事を読むにあたっての前提知識とします。
Server / Client Component
Next.js App Router の目新しい点といえば、 React Server Components が導入されたことだと思います。
「Server Component は制約がキツく、扱いづらい」と感じる人が多いと思うので、当記事ではそこにだけ絞り込んで解説します。
SSR の認識について注意 (2023/04/16 追記)
Client Component も SSR (Server Side Rendering) および SG (Static Generation) の対象になります。
このように、 Server / Client Component の境界線と SSR・SG / CSR (Client Side Rendering) の境界線は完全に一致するわけではないので、そこを認識しておく必要があると思います。
もちろん、 Server Component はクライアント側にレンダリング結果だけを返して蒸発するので、 サーバー側(あるいはビルドする端末) 上でしか利用できません。
Client Component は末端へ
上記の記事には 「we recommend moving Client Components to the leaves」 (Client Component を末端に寄せるのを推奨します) とあります。
次の節にあるように、 Client Component (以下 Client Comp.) から Server Component を利用することができないので、 コンポーネントツリー構造のルート(Root のほう) の側に Client Comp. を置いてしまうと、 Server Comp. を配置できる場所が限られてしまいます。
そうなると、ページの構成などによっては、 Server Comp. 特有のユーザー/開発者体験を享受する機会が失われてしまうことになります。
なので、できる限り Client Comp. の使用箇所をツリーの末端に寄せて、ルートに近いところは Server Comp. で固めるのがベストだと思います。
どうしてもツリーの構築が上手くいかない場合は、 Composition (children
Prop) を使えば解決するかもしれません。
🚫 Client → Server はダメ
Next.js の App Router では、 Client Component から Server Component を利用することが出来ないようになっています。
"use client"
function SomeClientComponent() {
// 🚫 DON'T: Client -> Server は利用できない
return <SomeServerComponent />
}
✅ children を使う
そこで children
prop (composition パターンとも呼ぶ) を活用すると、 Client Component の中に Server Component を配置することができます。
というより、Next.js でまともに Server Component の恩恵を受けるには composition が必須です。高等テクニックと思って避けずに、ガンガン使いましょう!
"use client"
type Props = { children: ReactNode }
function SomeClientComponent({ children }: Props) {
return <div>{children}</div>
}
function ParentServerComponent() {
return (
<SomeClientComponent>
{/* ✅ DO: children としてなら渡せる */}
<SomeServerComponent>
</SomeClientComponent>
);
}
output: 'export'
SG 静的生成: Next.js 13.2.x 以前では、 next export
コマンドを利用して SG (静的生成) 機能を利用していましたが、 Next.js 13.3.0 からは、 next.config.js
の output
オプションで指定することになります。
App Router だと、 next dev
で開発用サーバーを立ち上げて、対象のページを表示した時点で SG 不可能になる機能 の使用を検知して、エラーを表示してくれます。
新しく入った Server Component という名前の第一印象で、 サーバーが必要であるかのように思ってしまいますが、それとは裏腹に、 App Router は以前よりも SG フレンドリーになったと 思います。
ちなみに、 Server Component と SG を組み合わせると 「ビルド時にレンダリングされ、JS のバンドルを肥大させないコンポーネント」 が作れます。 Jamstack で Markdown をパースするようなケースでは恐らく最強です。
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
Static / Dynamic Rendering
Next.js の従来の page
ディレクトリでは、 (動的な) SSR がデフォルトで SG (静的生成) の機能が後付けされました。
Automatic Static Optimization という機能があることからも分かるように、あくまで「動的な要素がないときに、ビルド時に事前にHTMLを書き出してくれる」というものでした。
App Router では、うって変わって Static Rendering (静的レンダリング) がデフォルトになります。
Static / Dynamic はルート単位
Static / Dynamic Rendering はルート単位で決定されます。 (将来は更にレイアウト・ページも別々に設定できるようになるようです。)
Static Rendering がデフォルト
App Router では、 Static Rendering がデフォルトになり、ビルド時に確定する内容はビルド時に書き出されます。
注意が必要なのは、「動的なつもりの記述が静的になってしまう」ケースがあることです。
Server Component で fetch()
でデータを GET したり、 DB 等からデータ取得する場合には、デフォルトだと 「ビルド時に取得したっきりで、古い結果だけが表示されつづける」 ことになります。
リクエストのたびにデータを取得して欲しい場合は、 Dynamic Rendering (動的レンダリング) を有効化 する必要があります。
Dynamic Rendering の有効化
Dynamic Functions(動的な機能) または Dynamic Data Fetching (動的なデータ取得)を使ったときに、初めて Dynamic Rendering (動的レンダリング) が有効化されます。 (オプトイン)
Dynamic Functions (動的な機能)
- Server Component 内で使える
cookies()
,headers()
- このルート内全体がリクエスト時にレンダリングされる
- Client Component 内で使える
useSearchParams()
- 2023/05/05 この項目を修正
- Static Rendering されるルート / (Segment Configによって) Dynamic Rendering を有効化したルートの間で挙動が異なる
- 詳しくは公式ドキュメントを確認すべし
- https://nextjs.org/docs/app/api-reference/functions/use-search-params
-
page.tsx
でsearchParams
Prop を利用する
Dynamic Data Fetching (動的なデータ取得)
fetch()
を使った場合 、 fetch()
はデフォルトで Static Data Fetching (静的データ取得) として認識されるので cache: 'no-store'
オプションを渡すことで、Dynamic Data Fetching として認識させることができます。
Segment Config (セグメント単位の設定) として定数をエクスポートすることによっても制御できます。
fetch 以外によるデータ取得 (例えば、DB からの取得クエリの実行など) では、こちらの方法が必要になります。
// ⚠ これを設定しなかった場合、ずっとビルドを走らせた時刻が表示される。
export const dynamic = "force-dynamic";
const format = new Intl.DateTimeFormat("ja-JP", {
timeZone: "Asia/Tokyo",
timeStyle: "full",
});
export default function Page() {
// 現在時刻の取得 (fetch 以外のデータ取得の例)
const now = new Date();
return (
<div>
<div>Now: </div>
<div>{format.format(now)}</div>
</div>
);
}
Dynamic Segment はどうなる?
ごめんなさい、わかりません。
Dynamic Segment (params, useParams) が Static / Dynamic Rendering
にどう関わるのかについての明示的な記述がないので、何とも言えません。
generateStaticParams, dynamicParams などが絡むと Static になったり Dynamic になったりするので、公式は明言を避けているのでしょうか。
isReady は、もう要らない
Next.js で開発していてイライラするポイント No.1 が 「クエリパラメータや動的パスの取得が煩雑になる」 ことでした。(個人調べ)
App Router では、アーキテクチャの変更によって極めてシンプルになっています。 「isReady
を使って useEffect 内で条件分岐する」のはもう不要になりました。
app/(secure)
Route Groups: ディレクトリ名を () で囲うと Route Groups の機能が使用できます。
裏側ではファイルツリーと同く (secure)
というセグメントがあるかのように振る舞いますが、実際のアプリケーションのパスには現れません。
ファイルパス | 実際のパス |
---|---|
app/(non-secure)/page.tsx |
/ |
app/(non-secure)/privacy-policy/page.tsx |
/privacy-policy |
app/(secure)/admin/page.tsx |
/admin/ |
app/(secure)/users/hoge/page.tsx |
/users/hoge |
例えば、
-
/
と それ以外のパスでレイアウトを別々にするとき - 一つのセグメント以下で、レイアウトや共通処理を別々にするとき
-
/admin/**
,/users/**
はログインが必要、 それ以外のパスは全てログイン不要、のように
-
のようなケースで使用できます。
app/_components/
ルーティング対象外: Next.js App Router は _
から始まる名前のフォルダを、ルーティング制御から除外してくれます。
ルートに基づいてコードをまとめて配置 (Colocation) する都合上、複数のルートから呼び出されるコードの置き場所に困るかもしれません。
そのようなコードの置き場として、ルーティング対象外になる _something/
のようなディレクトリを利用することができます。
-
app/_utils/dete-time-format.ts
- 日付フォーマットの関数を export
-
app/_components/Button.ts
- 汎用ボタンコンポーネント
router.refresh()
Mutating データ更新 Server Component 上でデータを更新する方法については、 まだ仕様策定中(?) です。
ワークアラウンド (とりあえずの方法) としては、 router.refresh()
を使うことで実現できます。
Server Actions (Alpha)
データを更新するアクションについては、 Server Actions 機能として切り出されました。 この記事での解説の対象外とします。とりあえず公式のドキュメントを参照してください。
useRouter
の大幅変更
App Router の useRouter
は、 next/router
ではなく next/navigation
からインポートできます。
pathname, query については、このあとの章で解説するので読み進めてください。(もしくは目次からジャンプ)
router.events
は今のところサポート外です。(これは地味に痛い) useEffect で解決する場合はそちらを使いましょう。
/posts/[slug]
Dynamic Segments 動的セグメント:
ファイル名: /posts/[userId]/[slug]
実際のパス: /posts/honey32/next-13-app-overview
から { userId: "honey32", slug: "next-13-app-overview" }
を取り出したいとき、
従来の page
ルートでは、この動的セグメントとクエリパラメータが同じ router.query
オブジェクトに押し込まれていましたが、 App Router では扱い方が大きく異なります。
router.query
の扱いが非常に難しかったことを考えると、 App Router ではキチンと分離されて、Web 標準の機能だけで出来るようになって分かりやすくなったと思います。
href や push() で指定する方法
query オブジェクトを渡す、という方法は App Router では使えません。 自力で文字列を構築しましょう。
<Link href={`/posts/${post.userId}/${post.slug}`}>
ここに記事のタイトルを書く
</Link>
実験的機能の Statically Typed Links を用いれば、 TypeScript によってパス文字列に型チェックを掛けることができます。
この記事は逆引きで公式ドキュメントに誘導するだけにして、詳細は省きます。
params
prop
layout.tsx →
params
prop
page.tsx →
useParams()
hook
この Hook は、 Client Component であれば Layout / Page どちらからでも利用できます。
?limit=15
SearchParams クエリパラメータ: Dynamic Segments と同じく、クエリパラメータを扱う方法も大きく変わりました。
従来の page
ルートでは、この動的セグメントとクエリパラメータが同じ router.query
オブジェクトに押し込まれていましたが、 App Router では扱い方が大きく異なります。
href や push() で指定する方法
query オブジェクトを渡す、という方法は App Router では使えません。 URLSearchParams を活用して文字列を構築しましょう。
コンフィグを設定すると、型チェックが効くようになります。
const params = new URLSearchParams([
["user", "honey32"],
]);
<Link href={`/search?${params.toString()}`}>
リンクテキスト
</Link>
layout.tsx では読み取れない
⚠ Layout においては Search Params を読み取ることができません。
-
searchParams
prop もありません - Layout コンポーネントを Client Component にして
useSearchParams()
を使用できません。- Hydration Error が発生します。
- Layout コンポーネントが利用するコンポーネント内でも同様に使用不可です。
searchParams
prop
page.tsx → オブジェクト ({ key: string | string[] }
)として受け取れます。
useSearchParams()
hook
この Hook は Client Component から利用できます。
ただし、 Layout および Layout から利用されるコンポーネントでは使用できません。 Hydration Error が発生します。
イミュータブルな URLSearchParams
オブジェクトが返ります。
レイアウトにおいて相対的にパスを取得
レイアウトコンポーネントまたはそれに利用されるコンポーネントで以下の Hooks を使用して、 レイアウトの子のパスのうち、どれが選択されているか を取得することができます。
useSelectedLayoutSegment()
-> string | null
公式ドキュメントより引用
Layout Visited URL Returned Segment app/layout.js
/
null
app/layout.js
/dashboard
'dashboard'
app/dashboard/layout.js
/dashboard
null
app/dashboard/layout.js
/dashboard/settings
'settings'
app/dashboard/layout.js
/dashboard/analytics
'analytics'
app/dashboard/layout.js
/dashboard/analytics/monthly
'analytics'
useSelectedLayoutSegments()
-> string[]
ドキュメントより引用
Layout Visited URL Returned Segments app/layout.js
/
[]
app/layout.js
/dashboard
['dashboard']
app/layout.js
/dashboard/settings
['dashboard', 'settings']
app/dashboard/layout.js
/dashboard
[]
app/dashboard/layout.js
/dashboard/settings
['settings']
パスを取得(クエリパラメータは含まない)
usePathname()
この Hook は、 Layout においても Page においても利用できます。
Discussion
良い記事ありがとうございます!
は少し慣れが必要そうですね。。
page.tsx
ServerComponent.tsx
ClientComponent.tsx