Next.js App Router を使ってみて感じた革命的な変化

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>;
}

getServerSidePropsgetStaticPropsgetStaticPaths...覚えること多すぎ。

しかも、データ取得とコンポーネントが分離されてて、コードが散らばる。

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は送信しない

つまり:

  1. バンドルサイズが激減: サーバーコンポーネントのコードはブラウザに送られない
  2. 高速: ブラウザでの処理が最小限
  3. データアクセスが簡単: サーバー側で直接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>
  );
}

これで、このコンポーネントだけクライアント側で動きます。

使い分けの原則

私が実践している原則:

  1. デフォルトはServer Component: 特に理由がなければサーバー側で
  2. インタラクティブならClient Component: useStateonClickなどを使うなら
  3. 細かく分割: 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の素晴らしい点:

  1. Server Componentsが革命的: バンドルサイズ激減、パフォーマンス向上
  2. コードがシンプル: データ取得がコンポーネント内で完結
  3. レイアウトシステムが優秀: 入れ子構造で柔軟に管理
  4. 開発体験が良い: loading.tsx、error.txsで簡単にUI制御
  5. SEOに最適: サーバーサイドレンダリングが標準

こんな人におすすめ:

  • 新規プロジェクトを始める人: 迷わずApp Routerで
  • Pages Routerに不満がある人: 移行する価値あり
  • パフォーマンスを重視する人: バンドルサイズが激減
  • シンプルなコードが好きな人: データ取得がめちゃ楽

移行の難易度:

  • 学習コスト: 中程度(Server/Client Componentの概念を理解する必要あり)
  • 移行時間: 小規模なら1-2日、中規模なら1週間
  • リスク: Pages Routerも併用できるので低い

Next.jsでこれから開発するなら、App Router一択です。

最初は戸惑うかもしれませんが、慣れたらもう戻れません。


参考リンク:

このブログのソースコードも、そのうち公開するかもしれません。App Routerの実例として参考になれば!