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.tsxとerror.tsxは、 内部的にReactのSuspenseとErrorBoundaryでラップされます。
動的ルート(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.tsx、layout.tsx、loading.tsx、error.tsxなどの特殊ファイルがある[slug]で動的ルート、[...slug]でキャッチオールルートを作成できる- レイアウトはネストでき、ページ遷移時も状態が保持される
Linkコンポーネントでクライアントサイドナビゲーション、useRouterでプログラム的な遷移が可能- ルートグループ
(group)でURLに影響せずフォルダを整理できる