<CodeLearn/>
Next.js レッスン2

ファイルベースルーティング

App Routerの仕組みを理解し、動的ルートやレイアウトを使いこなそう

App Routerの基本

Next.jsのApp Routerでは、appディレクトリ内の フォルダ構造がそのままURLのルーティングになります。React Routerのように設定ファイルを書く必要はありません。

# フォルダ構造 → URL の対応
app/
├── page.tsx                    # → /
├── about/
│   └── page.tsx                # → /about
├── blog/
│   ├── page.tsx                # → /blog
│   └── [slug]/
│       └── page.tsx            # → /blog/my-first-post など
├── dashboard/
│   ├── layout.tsx              # → /dashboard/* の共通レイアウト
│   ├── page.tsx                # → /dashboard
│   └── settings/
│       └── page.tsx            # → /dashboard/settings
└── (marketing)/
    ├── pricing/
    │   └── page.tsx            # → /pricing (グループ名はURLに含まれない)
    └── contact/
        └── page.tsx            # → /contact

ルーティングに関わるのは特定の名前を持つファイルだけです。 それ以外のファイル(ユーティリティ関数、コンポーネントなど)を同じフォルダに置いても、URLには影響しません。

特殊ファイルの規約

App Routerでは、ファイル名に特別な意味があります。それぞれの役割を理解しましょう。

// page.tsx — ページのUIを定義(これがないとURLにアクセスできない)
export default function BlogPage() {
  return <h1>ブログ一覧</h1>;
}

// layout.tsx — 子ページに共通するレイアウト
// ページ遷移してもレイアウトは再レンダリングされない(状態が保持される)
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <aside>サイドバー</aside>
      <main>{children}</main>
    </div>
  );
}

// loading.tsx — ページ読み込み中に表示されるUI
export default function Loading() {
  return <div>読み込み中...</div>;
}

// error.tsx — エラー発生時に表示されるUI("use client" が必要)
"use client";
export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={reset}>再試行</button>
    </div>
  );
}

// not-found.tsx — 404ページ
export default function NotFound() {
  return <h2>ページが見つかりません</h2>;
}

これらのファイルは自動的にNext.jsに認識され、適切なタイミングで表示されます。 特にloading.tsxerror.tsxは、 内部的にReactのSuspenseErrorBoundaryでラップされます。

動的ルート(Dynamic Routes)

URLの一部が可変になるページには、フォルダ名を角括弧で囲みます。 例えばブログ記事ページでは、記事ごとに異なるURLが必要です。

// app/blog/[slug]/page.tsx
// /blog/hello-world → params.slug = "hello-world"
// /blog/nextjs-intro → params.slug = "nextjs-intro"

type Props = {
  params: { slug: string };
};

export default function BlogPost({ params }: Props) {
  return (
    <article>
      <h1>記事: {params.slug}</h1>
      <p>この記事のスラッグは「{params.slug}」です。</p>
    </article>
  );
}

// --- 複数のセグメントをキャッチする場合 ---

// app/docs/[...slug]/page.tsx(Catch-all)
// /docs/a → params.slug = ["a"]
// /docs/a/b/c → params.slug = ["a", "b", "c"]

type DocsProps = {
  params: { slug: string[] };
};

export default function DocsPage({ params }: DocsProps) {
  return <p>パス: {params.slug.join("/")}</p>;
}

// app/shop/[[...slug]]/page.tsx(Optional Catch-all)
// /shop → params.slug = undefined
// /shop/clothes → params.slug = ["clothes"]
// /shop/clothes/tops → params.slug = ["clothes", "tops"]

ネストされたレイアウト

レイアウトは入れ子にできます。各フォルダにlayout.tsxを 置くことで、そのフォルダ配下の全ページに共通のUIを追加できます。

// app/layout.tsx(ルートレイアウト)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <header>グローバルヘッダー</header>
        {children}
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx(ダッシュボード用レイアウト)
// ルートレイアウトの中にネストされる
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <nav className="w-64 bg-gray-100">
        <ul>
          <li><a href="/dashboard">概要</a></li>
          <li><a href="/dashboard/analytics">分析</a></li>
          <li><a href="/dashboard/settings">設定</a></li>
        </ul>
      </nav>
      <main className="flex-1 p-8">{children}</main>
    </div>
  );
}

// 最終的なHTML構造:
// <html>
//   <body>
//     <header>グローバルヘッダー</header>
//     <div class="flex">
//       <nav>サイドバー</nav>
//       <main>ページの内容</main>
//     </div>
//   </body>
// </html>

Linkコンポーネントとナビゲーション

ページ間のナビゲーションには、Next.jsのLinkコンポーネントとuseRouterフックを使います。 通常の<a>タグと違い、 ページ全体をリロードせずにクライアントサイドで遷移します。

// Linkコンポーネント(宣言的ナビゲーション)
import Link from "next/link";

export default function Navigation() {
  return (
    <nav>
      <Link href="/">ホーム</Link>
      <Link href="/about">アバウト</Link>
      <Link href="/blog">ブログ</Link>

      {/* 動的ルートへのリンク */}
      <Link href="/blog/my-first-post">最初の記事</Link>

      {/* prefetch を無効にする(大きなページの場合) */}
      <Link href="/heavy-page" prefetch={false}>重いページ</Link>
    </nav>
  );
}

// useRouter(プログラム的ナビゲーション)
"use client"; // useRouterはClient Componentでのみ使用可能
import { useRouter } from "next/navigation";

export default function LoginForm() {
  const router = useRouter();

  const handleLogin = async () => {
    // ログイン処理...
    const success = true;

    if (success) {
      router.push("/dashboard");    // ページ遷移
      // router.replace("/dashboard"); // 履歴を置き換える遷移
      // router.back();               // 前のページに戻る
      // router.refresh();            // 現在のページを再読み込み
    }
  };

  return <button onClick={handleLogin}>ログイン</button>;
}

ルートグループ

フォルダ名を括弧で囲むと、URLに影響しない「ルートグループ」を作れます。 ルートを論理的に整理したり、グループごとに異なるレイアウトを適用したい場合に便利です。

# ルートグループの例
app/
├── (marketing)/
│   ├── layout.tsx         # マーケティングページ用レイアウト
│   ├── about/
│   │   └── page.tsx       # → /about ("marketing" はURLに含まれない)
│   └── pricing/
│       └── page.tsx       # → /pricing
├── (app)/
│   ├── layout.tsx         # アプリページ用レイアウト(サイドバー付き)
│   ├── dashboard/
│   │   └── page.tsx       # → /dashboard
│   └── settings/
│       └── page.tsx       # → /settings
└── layout.tsx             # ルートレイアウト

# メリット:
# - マーケティングページとアプリページで異なるレイアウトを使える
# - フォルダを論理的に整理できる
# - URLの構造に影響しない

まとめ

  • App Routerではフォルダ構造がそのままURLのルーティングになる
  • page.tsxlayout.tsxloading.tsxerror.tsxなどの特殊ファイルがある
  • [slug]で動的ルート、[...slug]でキャッチオールルートを作成できる
  • レイアウトはネストでき、ページ遷移時も状態が保持される
  • Linkコンポーネントでクライアントサイドナビゲーション、useRouterでプログラム的な遷移が可能
  • ルートグループ(group)でURLに影響せずフォルダを整理できる