2.

Next.js App Router のルーティング入門

編集

この記事の要点

  • フォルダが URL のセグメントになり、page.tsx を置いた時点でそのパスが公開される。フォルダを作っただけでは URL にならない。
  • 角かっこで囲んだフォルダ名が動的セグメント。[slug] が 1 階層、[...slug] が全階層、[[...slug]] は省略可能。
  • バージョン 16 では paramssearchParamsPromise。必ず await してから中身を取り出す。
  • 丸かっこで囲んだ (group) は URL に出ない整理用フォルダ。アンダースコア始まりの _folder はルーティング対象から外れる。
  • ページ間の移動は next/linkLink。先読みとクライアント遷移が効く。

App Router のルーティングは「フォルダの形がそのまま URL の形になる」という単純な規則で成り立っています。ルーティング設定ファイルは存在せず、どんな URL が生えているかはディレクトリツリーを見れば分かります。この記事では、その対応関係と、実務でよく使う動的セグメント・整理用フォルダまでを押さえます。

1フォルダが URL になる

基本の規則は 2 つだけです。

  • フォルダが URL のセグメント(/ で区切られた 1 区画)を定義する
  • page.tsx を置いたフォルダだけが、外部からアクセスできる URL になる

裏を返すと、フォルダを作っただけでは URL は生えません。これは意図的な設計で、コンポーネントやユーティリティを route と同じ場所に置いても、うっかり公開されてしまう心配がないようになっています。

ファイルの場所対応する URL
app/page.tsx/
app/blog/page.tsx/blog
app/blog/authors/page.tsx/blog/authors
app/blog/utils.ts(URL にならない。同じ場所に置いても安全)

ページの実体は、React コンポーネントを default でエクスポートするだけです。

// app/blog/page.tsx
export default function Page() {
  return <h1>ブログ一覧</h1>
}

2動的セグメントで URL に値を埋める

記事 ID や商品コードのように値が可変な URL は、フォルダ名を角かっこで囲んで表現します。これを動的セグメントと呼びます。

フォルダマッチする URL用途
app/blog/[slug]/page.tsx/blog/hello1 階層ぶんの値
app/shop/[...slug]/page.tsx/shop/clothing/shop/clothing/shirts階層の深さが決まらないカテゴリなど
app/docs/[[...slug]]/page.tsx/docs と、その配下すべて省略も許したいドキュメント階層

受け取った値は params という props に入ります。ここがバージョン 16 での最大の変更点で、params は Promise になりました。同期的に params.slug と書くとエラーになります。

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params        // 必ず await する
  const post = await getPost(slug)

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  )
}

TypeScript を使っている場合は、型を手で書く代わりにグローバルヘルパーの PageProps が使えます。この型は next devnext build の実行時に自動生成され、インポートは不要です。

// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
  const { slug } = await props.params
  return <h1>記事: {slug}</h1>
}

3クエリ文字列を読む

?page=2 のようなクエリは searchParams で受け取ります。これも Promise です。

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
  const { page } = await searchParams
  // page を使ってデータを取りに行く
}

クエリはリクエストが届いて初めて確定する値なので、これを読むとそのページはリクエスト時に描画されることになります。使い分けの目安は次のとおりです。

searchParams を使う
ページャや絞り込みのように、クエリの値を使ってサーバー側でデータを取りに行くとき。
useSearchParams を使う
すでに読み込み済みの一覧をブラウザ側だけで絞り込むなど、クライアントで完結するとき。

4URL に出ないフォルダ

アプリが育つと「URL は変えたくないが、コードはグループごとに分けたい」という要求が出てきます。これに応えるのが 2 種類の特別なフォルダ名です。

app/ (marketing)/丸かっこ → URL に出ない about/page.tsx→ /about (shop)/ cart/page.tsx→ /cart layout.tsx→ (shop) 配下だけの共通レイアウト blog/ _components/Card.tsxアンダースコア → ルーティング対象外
(group) は URL を変えずにレイアウトを分けるため、_folder は確実にルーティングから外すために使う。

ルートグループ (group) はフォルダ名を丸かっこで囲む書き方です。URL には現れないので、app/(marketing)/about/page.tsx/about になります。グループごとに layout.tsx を置けるため、「公開ページと管理画面でヘッダーを変えたいが、URL の階層は増やしたくない」という場面で使います。

プライベートフォルダ _folder はアンダースコアで始めるフォルダで、そのフォルダと配下すべてがルーティングから除外されます。前述のとおり同居ファイルは元々 URL になりませんが、意図を明示したいときや、将来 Next.js が追加する予約ファイル名と衝突させたくないときに有効です。

5ページ間を移動する

画面遷移には next/linkLink コンポーネントを使います。見た目は a タグですが、リンク先の先読みとクライアント側での遷移が働き、ページ全体の再読み込みが起きません。

import Link from 'next/link'

export default async function Posts() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

素の a タグを使うとフルリロードになり、レイアウトの状態もスクロール位置も失われます。外部サイトへのリンク以外は Link を使うのが原則です。ボタンのクリック処理の中などから遷移したい場合は useRouter を使いますが、まずは Link で足りないかを検討してください。

6特殊な配置パターン

頻度は下がりますが、覚えておくと設計の幅が広がる規則もあります。

記法意味典型的な用途
@folder名前付きスロット(並行ルート)サイドバーと本文を別々に描画する
(.)folder同じ階層のルートを差し込む一覧の上に詳細をモーダルで重ねる
(..)folder親階層のルートを差し込む親の子ページをオーバーレイ表示する
(...)folderルート直下から差し込む任意のページを現在の画面内に出す

並行ルートを使う場合、バージョン 16 からはすべてのスロットに default.tsx が必須になりました。無いとビルドが失敗します。従来の挙動に合わせるなら、notFound() を呼ぶか null を返すファイルを置きます。

// app/@modal/default.tsx
export default function Default() {
  return null
}

7つまずきやすいところ

  • フォルダを作ったのに 404 になるpage.tsx が無いだけです。フォルダは URL を作るが、公開するのは pageroute の役目です。
  • params.slug が undefined になるawait params を忘れています。バージョン 15 まではそのまま読めたため、古い記事のコードが混ざると起きがちです。
  • ルートグループを重ねすぎる:URL に出ないぶん構造が見えにくくなります。レイアウトを分ける目的がないなら普通のフォルダで十分です。
  • 同じ階層に page.tsxroute.ts を置く:競合してビルドが通りません。API は別のパスに切り出します(Route Handlers で API を作る)。

URL の骨格ができたら、次はその周りを取り囲む共通レイアウトと、読み込み中・エラー時の表示を担当する特殊ファイルを見ていきます。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点