
Next.js App Router を使ってみて感じた革命的な変化
「App Routerって、結局Pages Routerと何が違うの?」
最初はそう思ってました。
Next.js 13でApp Routerが発表されたとき、「またディレクトリ構造が変わるだけでしょ」と冷めた目で見てた。pages/がapp/になるだけかな、みたいな。
完全に誤解してました。
App Routerは、単なるディレクトリ構造の変更じゃない。Reactの書き方そのものが変わる革命的なアップデートでした。
このブログもApp Routerで構築してます。実際に使ってみて分かった、その凄さを語ります。
Pages Routerの限界を感じた瞬間
Pages Routerは素晴らしいシステムでした。ファイルベースのルーティングは直感的だし、開発体験も良かった。
でも、使っているうちに、こんな不満が溜まってきました:
1. データ取得が面倒
// Pages Router の場合
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts }
};
}
export default function Page({ posts }) {
return <div>{/* ... */}</div>;
}
getServerSideProps、getStaticProps、getStaticPaths...覚えること多すぎ。
しかも、データ取得とコンポーネントが分離されてて、コードが散らばる。
2. クライアント側のJavaScriptが多い
Pages Routerでは、すべてのコンポーネントがクライアント側でも実行される前提。
つまり、全部のJavaScriptがブラウザに送られる。
結果:
- バンドルサイズが大きい
- 初期ロードが遅い
- インタラクティブになるまで時間がかかる
3. レイアウトの共有が難しい
// _app.tsx で全ページ共通のレイアウト
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
これだと、ページごとに違うレイアウトを適用するのが大変。
カスタムプロパティを渡したり、条件分岐したり...コードがぐちゃぐちゃになる。
App Routerとの出会い:衝撃だった
Pages Routerの不満を抱えながら、App Routerを試してみました。
完全に別物でした。
最初の感想:「え、こんなに簡単になるの?」
Server Componentsの衝撃
App Routerの最大の特徴は、Server Componentsがデフォルトということ。
// app/page.tsx - これはServer Component
export default async function Page() {
// サーバー側で実行される
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
見てください、この簡潔さ。
getServerSideProps不要- コンポーネント内で直接
async/await - データ取得とUIが同じ場所に
コードが圧倒的にシンプルになりました。
なぜServer Componentsは革命的なのか
従来のReactは、すべてクライアント側で動く前提でした。
サーバー → HTMLを生成 → ブラウザに送信
→ JavaScriptも送信
→ ブラウザでReactが再度レンダリング(Hydration)
これ、無駄が多いんです。
Server Componentsは、サーバーでレンダリングして、その結果だけをブラウザに送る。
サーバー → コンポーネントをレンダリング → 結果(HTML的な)をブラウザに送信
→ JavaScriptは送信しない
つまり:
- バンドルサイズが激減: サーバーコンポーネントのコードはブラウザに送られない
- 高速: ブラウザでの処理が最小限
- データアクセスが簡単: サーバー側で直接DBにアクセスできる
具体例:このブログの記事一覧
このブログの記事一覧ページは、Server Componentで作られています。
// app/blog/page.tsx
import { getAllPosts } from '@/lib/posts';
export default async function BlogPage() {
// サーバー側でファイルシステムから記事を取得
const posts = await getAllPosts();
return (
<div className="grid gap-6">
{posts.map(post => (
<article key={post.slug}>
<h2>{post.title}</h2>
<p>{post.description}</p>
</article>
))}
</div>
);
}
これ、Pages Routerだと:
// pages/blog.tsx (Pages Router)
export async function getStaticProps() {
const posts = await getAllPosts();
return { props: { posts } };
}
export default function BlogPage({ posts }) {
return (/* 同じUI */);
}
App Routerの方が、データ取得とUIが近くて読みやすい。
しかも、getAllPostsのコードはブラウザに送られない。バンドルサイズが小さくなる。
Client Componentsの使い分けが肝
「じゃあ、全部Server Componentで良いじゃん」と思うかもしれません。
それが違うんです。
インタラクティブな機能(ボタンのクリック、フォーム入力など)は、Client Componentが必要です。
Client Componentの使い方
'use client'ディレクティブを追加するだけ:
'use client'
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
これで、このコンポーネントだけクライアント側で動きます。
使い分けの原則
私が実践している原則:
- デフォルトはServer Component: 特に理由がなければサーバー側で
- インタラクティブならClient Component:
useState、onClickなどを使うなら - 細かく分割: Client Componentは必要最小限に
具体例:記事ページ
記事ページを例に見てみましょう。
// app/blog/[slug]/page.tsx (Server Component)
import { getPost } from '@/lib/posts';
import ShareButton from '@/components/ShareButton';
export default async function PostPage({ params }) {
// サーバー側でデータ取得
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* この部分だけClient Component */}
<ShareButton title={post.title} />
</article>
);
}
// components/ShareButton.tsx (Client Component)
'use client'
export default function ShareButton({ title }) {
const handleShare = () => {
navigator.share({ title });
};
return (
<button onClick={handleShare}>
シェアする
</button>
);
}
記事のコンテンツはServer Componentでレンダリング(高速、SEO最適)。
シェアボタンだけClient Component(インタラクティブ)。
最小限のJavaScriptだけがブラウザに送られる。
レイアウトの革命:入れ子構造
App Routerのもう1つの革命が、レイアウトシステムです。
layout.tsx の威力
layout.tsxを置くだけで、そのディレクトリ以下のすべてのページにレイアウトが適用されます。
app/
├── layout.tsx # ルートレイアウト(全ページ共通)
├── page.tsx # トップページ
└── blog/
├── layout.tsx # ブログ専用レイアウト
├── page.tsx # 記事一覧
└── [slug]/
└── page.tsx # 記事詳細
各ページのレイアウト構成:
- トップページ: ルートレイアウトのみ
- 記事一覧: ルートレイアウト + ブログレイアウト
- 記事詳細: ルートレイアウト + ブログレイアウト
実装例
// app/layout.tsx(ルートレイアウト)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
// app/blog/layout.tsx(ブログレイアウト)
export default function BlogLayout({ children }) {
return (
<div className="max-w-4xl mx-auto">
<nav>
<a href="/blog">記事一覧</a>
<a href="/blog/tags">タグ</a>
</nav>
{children}
</div>
);
}
Pages Routerでは、これをやろうとすると複雑になりました。
App Routerなら、ファイルを置くだけ。圧倒的にシンプルです。
その他の便利機能
1. loading.tsx で簡単ローディング
// app/blog/loading.tsx
export default function Loading() {
return <div>読み込み中...</div>;
}
これだけで、ページ遷移時に自動でローディング表示されます。
2. error.tsx でエラーハンドリング
// app/blog/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>エラーが発生しました</h2>
<button onClick={reset}>再試行</button>
</div>
);
}
エラーが起きたら、このコンポーネントが表示されます。
3. 並列データ取得
Server Componentでは、複数のデータ取得を並列実行できます:
export default async function Page() {
// 同時に実行される
const [posts, tags] = await Promise.all([
getPosts(),
getTags(),
]);
return (/* ... */);
}
Pages Routerでは、これをやるのが結構面倒でした。
移行して感じたメリット
このブログをPages RouterからApp Routerに移行して、こんなメリットがありました。
1. コードが圧倒的にシンプルに
Before(Pages Router): 約500行
After(App Router): 約350行(30%削減)
データ取得の記述が減り、レイアウトの共有が簡単になった結果です。
2. パフォーマンスが向上
JavaScript バンドルサイズ:
- Before: 180KB
- After: 95KB(約47%削減)
Server Componentsのおかげで、クライアント側のJavaScriptが激減。
3. 開発速度が上がった
新しいページを追加するのが速くなりました。
- データ取得がシンプル
- レイアウトが自動適用
- ローディング・エラー処理も簡単
4. SEOが改善
Server Componentsは完全にサーバー側でレンダリングされるので、SEOに最適。
クローラーは完全にレンダリングされたHTMLを受け取れます。
移行時にハマったポイント
良いことばかりじゃなく、移行時に苦労したこともあります。
1. 'use client' の付け忘れ
// これはエラーになる(Server ComponentでuseStateは使えない)
import { useState } from 'react';
export default function Component() {
const [count, setCount] = useState(0); // エラー
return <div>{count}</div>;
}
エラーメッセージ:
You're importing a component that needs useState.
It only works in a Client Component but none of its parents are marked with "use client"
最初、このエラーで何度も引っかかりました。
解決策: 'use client'を追加するだけ。
2. クライアントコンポーネントの肥大化
最初、「とりあえず'use client'つけとこう」と、大きなコンポーネントをClient Componentにしてしまいました。
結果、バンドルサイズが大きくなる。
教訓: Client Componentは必要最小限に。細かく分割する。
3. キャッシュの挙動
App Routerは積極的にキャッシュします。
開発中、「あれ、変更したのに反映されない...」ってことが何度も。
解決策: 開発サーバーを再起動するか、キャッシュをクリア。
まとめ:App Routerは未来
Pages RouterからApp Routerに移行して、本当に良かったです。
App Routerの素晴らしい点:
- Server Componentsが革命的: バンドルサイズ激減、パフォーマンス向上
- コードがシンプル: データ取得がコンポーネント内で完結
- レイアウトシステムが優秀: 入れ子構造で柔軟に管理
- 開発体験が良い: loading.tsx、error.txsで簡単にUI制御
- SEOに最適: サーバーサイドレンダリングが標準
こんな人におすすめ:
- 新規プロジェクトを始める人: 迷わずApp Routerで
- Pages Routerに不満がある人: 移行する価値あり
- パフォーマンスを重視する人: バンドルサイズが激減
- シンプルなコードが好きな人: データ取得がめちゃ楽
移行の難易度:
- 学習コスト: 中程度(Server/Client Componentの概念を理解する必要あり)
- 移行時間: 小規模なら1-2日、中規模なら1週間
- リスク: Pages Routerも併用できるので低い
Next.jsでこれから開発するなら、App Router一択です。
最初は戸惑うかもしれませんが、慣れたらもう戻れません。
参考リンク:
このブログのソースコードも、そのうち公開するかもしれません。App Routerの実例として参考になれば!