こんにちは!Next.js 13以降で導入されたApp Routerは、従来のPages Routerとはまったく異なるアプローチでルーティングやレンダリングを管理します。すでに安定版として広く使われているApp Routerの基本を、これから移行する方向けに解説します。
App Router vs Pages Routerの違い
| 項目 | Pages Router | App Router |
|---|---|---|
| ルーティング | pages/ 配下 | app/ 配下 |
| コンポーネント | デフォルトでClient Component | デフォルトでServer Component |
| レイアウト | 個別に実装 | ネスト可能な layout.js |
| ローディング | 手動実装 | loading.js で自動 |
| データ取得 | getServerSideProps など | コンポーネント内で直接 async |
1. ファイルベースルーティングの基本
app/ ディレクトリ内のフォルダ構造がそのままURLパスになります。
app/
├── page.js → /
├── layout.js → 全ページ共通レイアウト
├── about/
│ └── page.js → /about
└── blog/
├── page.js → /blog
└── [slug]/
└── page.js → /blog/hello-world(動的ルート)
// app/blog/[slug]/page.js
export default async function BlogPost({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
2. レイアウトシステム
layout.js はそのフォルダ以下の全ページに自動で適用されます。ネストも可能です。
// app/layout.js(ルートレイアウト)
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<header>サイト共通ヘッダー</header>
{children}
<footer>サイト共通フッター</footer>
</body>
</html>
);
}
// app/blog/layout.js(ブログセクションのみのレイアウト)
export default function BlogLayout({ children }) {
return (
<div className="blog-wrapper">
<aside>サイドバー</aside>
<main>{children}</main>
</div>
);
}
3. ローディングとエラー状態
loading.js と error.js を配置するだけで、ローディングUIとエラーUIを自動的に表示できます。
// app/blog/loading.js
export default function Loading() {
return <div>読み込み中...</div>;
}
// app/blog/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>エラーが発生しました</h2>
<button onClick={reset}>再試行</button>
</div>
);
}
4. Server ComponentとClient Componentの使い分け
App Routerでは、デフォルトがServer Componentです。これは重要な変更点です。
| Server Component | Client Component | |
|---|---|---|
| レンダリング場所 | サーバー | ブラウザ |
| useState / useEffect | ❌ 使えない | ✅ 使える |
| データベース直接アクセス | ✅ 可能 | ❌ できない |
| ファイルサイズ | 小さい(JS不送) | 大きい |
| インタラクション | ❌ できない | ✅ できる |
// これは Server Component(デフォルト)→ DBアクセスできる
export default async function PostList() {
const posts = await db.query('SELECT * FROM posts');
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// クライアント側でインタラクションが必要な場合のみ 'use client'
'use client';
export default function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>♥</button>;
}
基本方針: できるだけServer Componentで書き、インタラクションが必要な最小限のコンポーネントだけClient Componentにします。
5. データフェッチングのパターン
App Routerでは fetch() をコンポーネント内で直接呼び出せます。デフォルトでキャッシュされます。
// キャッシュあり(デフォルト)
const data = await fetch('https://api.example.com/posts');
// キャッシュなし(毎回再取得)
const data = await fetch('https://api.example.com/posts', {
cache: 'no-store',
});
// 一定時間で再検証
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 60秒ごとに再検証
});
6. Route Groups(ルートグループ)
( ) でフォルダ名を囲むと、URLに影響を与えずにファイルを整理できます。
app/
├── (marketing)/
│ ├── page.js → /
│ └── pricing/
│ └── page.js → /pricing
└── (shop)/
├── layout.js → ショップ専用レイアウト
└── products/
└── page.js → /products
マーケティングサイトとショップで異なるレイアウトを使いたい場合などに便利です。
まとめ
App RouterはServer Componentを軸にした新しいアーキテクチャで、従来のPages Routerとは考え方が大きく異なります。最初は混乱するかもしれませんが、「Server Componentを基本に、必要な部分だけClient Component」という方針で書いていけば、パフォーマンスの高いアプリケーションが作れます。移行は小さなページから徐々に進めるのがおすすめです。
今日も最後までお読みいただき、ありがとうございました!

