Next.jsのルーティング詳細
Next.jsのルーティング詳細
Section titled “Next.jsのルーティング詳細”Next.jsは、ファイルシステムに基づいたルーティングを採用しています。これにより、プロジェクト内のディレクトリやファイルがそのままURLパスになります。Next.js 13以降、新しいApp Routerが導入され、既存のPages Routerと異なるアプローチでルーティングを管理します。
App RouterとPages Routerの違い
Section titled “App RouterとPages Routerの違い”| 特徴 | Pages Router | App Router |
|---|---|---|
| ディレクトリ | pages/ | app/ |
| 動的セグメント | [name].tsx | [name]/page.tsx |
| パス取得 | useRouter().query | paramsプロパティ |
| サーバーコンポーネント | 非対応 | 対応 |
| レイアウト共有 | _app.tsx | layout.tsx |
| データフェッチング | getServerSideProps, getStaticProps | asyncコンポーネント |
| 推奨 | 既存プロジェクト | 新規プロジェクト |
App Routerによるルーティング
Section titled “App Routerによるルーティング”App Routerは、app/ディレクトリを使用してルーティングを管理します。このアプローチでは、レイアウトの共有、ストリーミング、サーバーコンポーネントの利用が容易になります。
ファイルベースのルーティング (app/)
Section titled “ファイルベースのルーティング (app/)”app/ディレクトリ内のサブディレクトリがURLパスに対応します。各セグメントのpage.tsxファイルが、そのパスのUIを定義します。
ファイルシステムとURLの対応関係
Section titled “ファイルシステムとURLの対応関係”以下の図は、ファイルシステムの構造がどのようにURLパスにマッピングされるかを示しています。
ファイルシステム構造 URLパス───────────────────────────── ─────────────────────────app/├── page.tsx → /├── about/│ └── page.tsx → /about├── blog/│ ├── page.tsx → /blog│ ├── [slug]/│ │ └── page.tsx → /blog/[slug]│ └── [slug]/│ └── [commentId]/│ └── page.tsx → /blog/[slug]/[commentId]└── shop/ ├── [category]/ │ └── [productId]/ │ └── page.tsx → /shop/[category]/[productId] └── [[...slug]]/ └── page.tsx → /shop/[[...slug]]重要なポイント:
- ディレクトリ名 = URLセグメント: ディレクトリ名がそのままURLパスのセグメントになります
page.tsx= ページコンポーネント: 各ルートにはpage.tsxファイルが必要です[param]= 動的セグメント: 角括弧で囲まれた名前が動的パラメータになります[[...slug]]= オプショナルキャッチオール: 二重角括弧はオプショナルなキャッチオールルートです
ディレクトリ構造の例
Section titled “ディレクトリ構造の例”/app├── layout.tsx // 共有レイアウト(例: ナビゲーションバー)├── page.tsx // ルートページ (/)├── about/│ └── page.tsx // /aboutページ└── blog/ ├── layout.tsx // /blog配下の共通レイアウト ├── page.tsx // /blogページ └── [slug]/ └── page.tsx // /blog/[slug]ページ基本的なページの作成
Section titled “基本的なページの作成”export default function AboutPage() { return ( <div> <h1>About Us</h1> <p>私たちについて</p> </div> );}特徴:
- 自動ルーティング:
app/about/page.tsxが自動的に/aboutルートになる - デフォルトエクスポート:
page.tsxはデフォルトエクスポートが必要 - サーバーコンポーネント: デフォルトでサーバーコンポーネントとして動作
動的ルーティング
Section titled “動的ルーティング”動的なパスセグメントは、角括弧[]で囲んで指定します。例えば、[slug]というディレクトリは、/blog/first-postや/blog/second-postのようなURLに対応します。
単一の動的セグメント
Section titled “単一の動的セグメント”Next.js 15の変更点: Next.js 15以降では、
paramsはPromiseとして扱われるため、awaitが必要です。Next.js 14以前でも動作するように、awaitを使用することを推奨します。
type BlogPostPageProps = { params: Promise<{ slug: string; }>;};
export default async function BlogPostPage({ params }: BlogPostPageProps) { // Next.js 15以降: paramsをawaitする必要がある const { slug } = await params;
// サーバーコンポーネントなので、直接データフェッチングが可能 const post = await fetch(`https://api.example.com/posts/${slug}`) .then(res => res.json());
return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> );}特徴:
paramsプロパティ: 動的なセグメントの値がparamsプロパティとして渡される(Next.js 15以降はPromise)- 型安全性: TypeScriptで型定義が可能
- サーバーコンポーネント:
async関数として定義可能 - Next.js 15対応:
paramsをawaitで解決する必要がある
複数の動的セグメント
Section titled “複数の動的セグメント”type ProductPageProps = { params: Promise<{ category: string; product: string; }>;};
export default async function ProductPage({ params }: ProductPageProps) { // Next.js 15以降: paramsをawaitする必要がある const { category, product } = await params;
return ( <div> <h1>カテゴリ: {category}</h1> <h2>商品: {product}</h2> </div> );}URL例:
/shop/electronics/iphone→category: 'electronics',product: 'iphone'/shop/clothing/t-shirt→category: 'clothing',product: 't-shirt'
💡 シニアの知恵: なぜ意味のある名前を使うのか?
ネストされた動的ルートでは、すべてのパラメータが
paramsオブジェクトに含まれます。例えば、/users/[userId]/posts/[postId]という構造の場合、paramsは{ userId: '...', postId: '...' }となります。すべてを
[id]にしてしまうと、どのidがどのセグメントを指しているのかわからなくなり、バグの原因になります。「その変数が指す固有の名前」(userId、postIdなど)を常に使いましょう。
// ✅ 良い例: 意味のある名前を使用type PostPageProps = { params: Promise<{ userId: string; // ユーザーIDであることが明確 postId: string; // 投稿IDであることが明確 }>;};
export default async function PostPage({ params }: PostPageProps) { const { userId, postId } = await params; // userIdとpostIdの区別が明確 const post = await fetch(`/api/users/${userId}/posts/${postId}`); // ...}
// ❌ 悪い例: すべて[id]にしてしまう// app/users/[id]/posts/[id]/page.tsxtype PostPageProps = { params: Promise<{ id: string; // どちらのIDかわからない! }>;};
export default async function PostPage({ params }: PostPageProps) { const { id } = await params; // これはユーザーIDなのか、投稿IDなのかわからない // バグの原因になる可能性が高い}キャッチオールルート
Section titled “キャッチオールルート”type DocsPageProps = { params: Promise<{ slug: string[]; }>;};
export default async function DocsPage({ params }: DocsPageProps) { // Next.js 15以降: paramsをawaitする必要がある const { slug } = await params;
// slugは配列として渡される // /docs/getting-started/installation → ['getting-started', 'installation'] const path = slug.join('/');
return <div>ドキュメント: {path}</div>;}URL例:
/docs/getting-started→slug: ['getting-started']/docs/getting-started/installation→slug: ['getting-started', 'installation']
オプショナルキャッチオールルート
Section titled “オプショナルキャッチオールルート”type ShopPageProps = { params: Promise<{ slug?: string[]; }>;};
export default async function ShopPage({ params }: ShopPageProps) { // Next.js 15以降: paramsをawaitする必要がある const { slug } = await params;
if (!slug) { return <div>ショップトップページ</div>; }
return <div>ショップ: {slug.join('/')}</div>;}URL例:
/shop→slug: undefined(オプショナル)/shop/electronics→slug: ['electronics']/shop/electronics/phones→slug: ['electronics', 'phones']
レイアウトの共有
Section titled “レイアウトの共有”layout.tsxファイルを使用して、複数のページで共通のレイアウトを共有できます。
// app/layout.tsx(ルートレイアウト)export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="ja"> <body> <header> <nav> <a href="/">ホーム</a> <a href="/about">About</a> <a href="/blog">Blog</a> </nav> </header> <main>{children}</main> <footer> <p>© 2024 My App</p> </footer> </body> </html> );}// app/blog/layout.tsx(ブログ専用レイアウト)export default function BlogLayout({ children,}: { children: React.ReactNode;}) { return ( <div className="blog-container"> <aside> <h2>カテゴリ</h2> <ul> <li><a href="/blog/tech">技術</a></li> <li><a href="/blog/life">生活</a></li> </ul> </aside> <article>{children}</article> </div> );}特徴:
- ネストされたレイアウト: レイアウトは階層的に適用される
- レイアウトの永続化: ページ遷移時もレイアウトは再レンダリングされない
- 状態の保持: レイアウト内の状態はページ遷移時も保持される
ルートグループ
Section titled “ルートグループ”ルートグループを使用して、URLパスに影響を与えずにルートを整理できます。
/app├── (marketing)/│ ├── about/│ │ └── page.tsx // /about│ └── contact/│ └── page.tsx // /contact└── (shop)/ ├── products/ │ └── page.tsx // /products └── cart/ └── page.tsx // /cart特徴:
- URLに影響しない: 括弧で囲まれたディレクトリはURLパスに含まれない
- レイアウトの分離: 異なるレイアウトを適用できる
- 組織化: 関連するルートをグループ化できる
@folderという命名規則を使用して、同じURLに対して複数のページを同時に表示できます。
/app└── dashboard/ ├── @analytics/ │ └── page.tsx ├── @team/ │ └── page.tsx └── page.tsxexport default function DashboardLayout({ analytics, team, children,}: { analytics: React.ReactNode; team: React.ReactNode; children: React.ReactNode;}) { return ( <div> {children} {analytics} {team} </div> );}条件付きルート
Section titled “条件付きルート”loading.tsxとerror.tsxを使用して、ローディング状態とエラー状態を処理できます。
export default function Loading() { return <div>読み込み中...</div>;}'use client';
export default function Error({ error, reset,}: { error: Error; reset: () => void;}) { return ( <div> <h2>エラーが発生しました</h2> <button onClick={reset}>再試行</button> </div> );}クライアントコンポーネントでのルーティング
Section titled “クライアントコンポーネントでのルーティング”App Routerでは、クライアントコンポーネントでナビゲーションを行う場合、next/navigationからuseRouterをインポートする必要があります。これは、Pages Routerのnext/routerとは別物です。
⚠️ 重要な注意点:
next/router(Pages Router用)とnext/navigation(App Router用)は完全に別のパッケージです。混同しないように注意してください。
App Routerでのクライアントサイドルーティング
Section titled “App Routerでのクライアントサイドルーティング”'use client';
// ✅ 正しい: App Routerでは next/navigation を使用import { useRouter, usePathname, useSearchParams } from 'next/navigation';
export default function NavigationButton() { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams();
const handleClick = () => { // プログラムによるナビゲーション router.push('/about'); // または router.replace('/contact'); };
return ( <div> <p>現在のパス: {pathname}</p> <button onClick={handleClick}>Aboutページへ</button> </div> );}App RouterのuseRouter(next/navigation)の特徴:
- ✅
push(): 新しいページに遷移(ブラウザ履歴に追加) - ✅
replace(): 現在のページを置き換え(ブラウザ履歴に追加しない) - ✅
refresh(): 現在のルートを再フェッチ - ✅
back(): ブラウザの戻るボタンと同じ動作 - ✅
forward(): ブラウザの進むボタンと同じ動作 - ❌
queryプロパティは存在しない(これはPages Routerの機能)
Pages Routerでのクライアントサイドルーティング
Section titled “Pages Routerでのクライアントサイドルーティング”// ✅ 正しい: Pages Routerでは next/router を使用import { useRouter } from 'next/router';
export default function NavigationButton() { const router = useRouter();
// Pages RouterのuseRouterはqueryプロパティを持つ const { id, category } = router.query;
const handleClick = () => { router.push('/about'); };
return ( <div> <p>クエリパラメータ: {JSON.stringify(router.query)}</p> <button onClick={handleClick}>Aboutページへ</button> </div> );}Pages RouterのuseRouter(next/router)の特徴:
- ✅
push(): 新しいページに遷移 - ✅
replace(): 現在のページを置き換え - ✅
query: URLのクエリパラメータと動的ルートのパラメータにアクセス可能 - ✅
pathname: 現在のパス名 - ✅
asPath: クエリ文字列を含む実際のパス
比較表: App Router vs Pages Router
Section titled “比較表: App Router vs Pages Router”| 機能 | App Router (next/navigation) | Pages Router (next/router) |
|---|---|---|
| インポート | import { useRouter } from 'next/navigation' | import { useRouter } from 'next/router' |
push() | ✅ あり | ✅ あり |
replace() | ✅ あり | ✅ あり |
query | ❌ なし | ✅ あり |
pathname | usePathname()フックを使用 | ✅ router.pathname |
searchParams | useSearchParams()フックを使用 | ✅ router.query |
よくある間違い: インポートパスの混同
Section titled “よくある間違い: インポートパスの混同”'use client';
// ❌ 間違い: App Routerで next/router を使用import { useRouter } from 'next/router'; // エラーが発生する可能性
// ✅ 正しい: App Routerでは next/navigation を使用import { useRouter } from 'next/navigation';
export default function MyComponent() { const router = useRouter(); // ...}App Routerでクエリパラメータを取得する方法
Section titled “App Routerでクエリパラメータを取得する方法”App Routerでは、queryプロパティが存在しないため、useSearchParamsフックを使用します。
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchPage() { const searchParams = useSearchParams();
// クエリパラメータを取得 const query = searchParams.get('q'); const category = searchParams.get('category');
return ( <div> <p>検索クエリ: {query}</p> <p>カテゴリ: {category}</p> </div> );}URL例: /search?q=nextjs&category=framework
searchParams.get('q')→'nextjs'searchParams.get('category')→'framework'
Pages Routerによるルーティング
Section titled “Pages Routerによるルーティング”Pages Routerは、pages/ディレクトリを使用してルーティングを管理する従来のアプローチです。
ファイルベースのルーティング (pages/)
Section titled “ファイルベースのルーティング (pages/)”pages/ディレクトリ内のファイル名がURLパスになります。
/pages├── index.tsx // ルートページ (/)├── about.tsx // /aboutページ└── blog/ ├── index.tsx // /blogページ └── [id].tsx // /blog/[id]ページ基本的なページの作成
Section titled “基本的なページの作成”export default function AboutPage() { return ( <div> <h1>About Us</h1> <p>私たちについて</p> </div> );}動的ルーティング
Section titled “動的ルーティング”Pages Routerでは、useRouterフックを使って動的なセグメントの値を取得します。
import { useRouter } from 'next/router';
const BlogPostPage = () => { const router = useRouter(); const { id } = router.query;
// クエリパラメータも取得可能 const { category } = router.query;
return ( <div> <h1>ブログ投稿: {id}</h1> {category && <p>カテゴリ: {category}</p>} </div> );};
export default BlogPostPage;特徴:
useRouterフック: クライアントサイドでのルーティング情報にアクセス- クエリパラメータ: URLのクエリパラメータも
router.queryで取得可能 - クライアントコンポーネント: デフォルトでクライアントコンポーネント
データフェッチング
Section titled “データフェッチング”Pages Routerでは、getServerSidePropsやgetStaticPropsを使用してデータをフェッチします。
import { GetServerSideProps } from 'next';
type BlogPostPageProps = { post: { id: string; title: string; content: string; };};
export default function BlogPostPage({ post }: BlogPostPageProps) { return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> );}
export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params!;
const post = await fetch(`https://api.example.com/posts/${id}`) .then(res => res.json());
return { props: { post, }, };};ルーティングのベストプラクティス
Section titled “ルーティングのベストプラクティス”1. App Routerを優先する
Section titled “1. App Routerを優先する”新しいプロジェクトを始める場合は、App Routerの利用が強く推奨されます。App Routerは、将来的なNext.jsの進化の方向性であり、サーバーコンポーネントやデータフェッチの新しい方法など、多くのパフォーマンス改善を提供します。
2. 動的ルートの命名規則(思想の深掘り)
Section titled “2. 動的ルートの命名規則(思想の深掘り)”動的ルートの命名は、コードの可読性と保守性に大きく影響します。特に、ネストされたルートでは、意味のある名前を使用することが重要です。
なぜ[id]を避けるべきか?
Section titled “なぜ[id]を避けるべきか?”Next.jsのApp Routerでは、paramsがネストされます。例えば、/users/[userId]/posts/[postId]という構造の場合、paramsは{ userId: '...', postId: '...' }となります。
すべてを[id]にしてしまうと、どちらのIDかわからず、バグの原因になります。**「その変数が指す固有の名前」**を常に使いましょう。
// ✅ 良い例: 意味のある名前を使用app/blog/[slug]/page.tsxapp/users/[userId]/page.tsxapp/products/[productId]/page.tsxapp/users/[userId]/posts/[postId]/page.tsx
// ❌ 悪い例: 意味のない名前app/blog/[id]/page.tsxapp/users/[id]/page.tsxapp/users/[id]/posts/[id]/page.tsx // どちらのIDかわからない!ネストされたルートでの実例
Section titled “ネストされたルートでの実例”// ✅ 良い例: 明確な命名type PostPageProps = { params: Promise<{ userId: string; // ユーザーIDであることが明確 postId: string; // 投稿IDであることが明確 }>;};
export default async function PostPage({ params }: PostPageProps) { const { userId, postId } = await params;
// userIdとpostIdの区別が明確で、コードが読みやすい const post = await fetch(`/api/users/${userId}/posts/${postId}`); return <div>{/* ... */}</div>;}
// ❌ 悪い例: すべて[id]にしてしまう// app/users/[id]/posts/[id]/page.tsxtype PostPageProps = { params: Promise<{ id: string; // どちらのIDかわからない! }>;};
export default async function PostPage({ params }: PostPageProps) { const { id } = await params; // これはユーザーIDなのか、投稿IDなのかわからない // バグの原因になる可能性が高い const post = await fetch(`/api/users/${id}/posts/${id}`); // 間違っている可能性 return <div>{/* ... */}</div>;}命名規則のベストプラクティス
Section titled “命名規則のベストプラクティス”- 固有の名前を使用:
userId、postId、productIdなど、その変数が何を指しているか明確に - 複数形を避ける:
[users]ではなく[userId]を使用(単一のIDを表すため) - 意味のある名前:
[slug]、[username]、[category]など、その変数の意味が伝わる名前 - 一貫性を保つ: プロジェクト全体で命名規則を統一
3. レイアウトの適切な使用
Section titled “3. レイアウトの適切な使用”// ✅ 良い例: 共通レイアウトを使用app/layout.tsx // 全体のレイアウトapp/dashboard/layout.tsx // ダッシュボード専用レイアウト
// ❌ 悪い例: 各ページで個別にレイアウトを実装app/dashboard/page.tsx // レイアウトを含むapp/dashboard/settings/page.tsx // 同じレイアウトを再度実装4. エラーハンドリング
Section titled “4. エラーハンドリング”'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> );}5. ローディング状態の処理
Section titled “5. ローディング状態の処理”export default function Loading() { return ( <div> <div className="skeleton">読み込み中...</div> </div> );}Next.jsのルーティングは、ファイルシステムベースで直感的に理解できます。App RouterとPages Routerの違いを理解し、プロジェクトの要件に応じて適切なアプローチを選択することが重要です。
推奨事項:
- 新規プロジェクト: App Routerを使用
- 既存プロジェクト: Pages RouterからApp Routerへの移行を検討
- 動的ルート: 意味のある名前を使用
- レイアウト: 共通レイアウトを適切に使用
- エラーハンドリング:
error.tsxとloading.tsxを活用
これらのベストプラクティスを守ることで、保守性が高く、パフォーマンスの良いNext.jsアプリケーションを構築できます。