T-CREATOR

Next.js 13 App Router入門:Pages Routerとの違いと移行のコツをわかりやすく紹介

Next.js 13 App Router入門:Pages Routerとの違いと移行のコツをわかりやすく紹介

Next.js 13 App Router入門:Pages Routerとの違いと移行のコツについて何回かに分けて紹介していこうと思います。

革新的なルーティング構造の登場

Next.js 13から導入されたApp Routerは、これまでのPages Routerとは異なる新たなルーティング方式です。

React 18のServer ComponentsStreamingに完全対応し、より柔軟かつ高速なアプリケーション構築を可能にします。

しかし、新しい概念が多いため、「何が変わったのか」「どうやって移行すれば良いのか」と迷われている方も多いのではないでしょうか。

本記事では、初心者の方にもわかりやすく、App RouterとPages Routerの違いや移行方法を、実際のコードを交えて丁寧に解説してまいります。


App Routerとは何か?

まずは、App Routerの概要を押さえましょう。

Next.jsの公式ドキュメントでは以下のように説明されています:

The App Router introduces a new routing system built on top of React Server Components, supports layouts, and provides enhanced flexibility.
Next.js App Router Docs

要点をまとめると、App Routerは以下の特徴を持っています。

特徴内容
ディレクトリベースのルーティング/app以下にフォルダとファイルを配置することでルーティングを定義
Layoutによる共通UI構造各ページで共通レイアウトを容易に適用可能
Server Component対応パフォーマンス向上のために、デフォルトでサーバーサイドでレンダリング
Streaming対応クライアントに逐次レンダリングして表示速度を向上
File Conventionの刷新page.tsx, layout.tsx, loading.tsxなどファイルによって意味が決定する

では、これらの特徴をそれぞれ詳しく見ていきましょう。


ディレクトリ構造の違い

Pages Routerの場合

従来のPages Routerでは、ルーティングはpages/ディレクトリ内に.tsxファイルを配置することで構成されていました。

例:Pages Routerの構成

bash/pages
  ├─ index.tsx         → ルート (/)
  ├─ about.tsx         → /about
  └─ blog/
       └─ [id].tsx     → 動的ルーティング (/blog/1など)

この構成ではReactのコンポーネントとURLの関係が直感的ですが、レイアウトの共有やサーバーコンポーネントの導入には限界がありました。


App Routerの場合

App Routerでは、すべてのルート定義が/appディレクトリで構成されます。

例:App Routerの構成

bash/app
  ├─ page.tsx            → ルート (/)
  ├─ about/
       └─ page.tsx       → /about
  ├─ blog/
       ├─ [id]/
       │    └─ page.tsx  → 動的ルーティング (/blog/1)
       └─ layout.tsx     → blog配下の共通レイアウト

これにより、ディレクトリ単位でルートやレイアウトを柔軟に管理でき、モジュール化された設計が可能になります。


page.tsxの基本

App Routerでは各ルートのページコンポーネントは必ずpage.tsxとして定義します。

例:基本的なページ定義

tsx// app/about/page.tsx

export default function AboutPage() {
  return <h1>Aboutページです</h1>;
}

このファイルを作成するだけで、/aboutというURLが自動的に生成されます。

とてもシンプルで、Pages Routerに慣れている方でも違和感なく使い始められるでしょう。


layout.tsxで共通レイアウトを実現

App Routerでは、レイアウトを共通化する際にlayout.tsxというファイルを使います。

このファイルはディレクトリ単位で機能し、配下のすべてのルートに適用されます。

例:共通レイアウトの実装

tsx// app/blog/layout.tsx

export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <h2>Blogページ共通の見出し</h2>
      <main>{children}</main>
    </div>
  );
}

これにより、/blog/以下のすべてのページでこのレイアウトが使用されます。


loading.tsxによるローディングUI

App Routerではページの読み込み中に表示されるローディングUIをloading.tsxで定義できます。

例:ローディング画面の定義

tsx// app/blog/loading.tsx

export default function Loading() {
  return <p>読み込み中です...</p>;
}

ルートの読み込みが完了するまでこのUIが表示されるため、ユーザー体験の向上につながります。


動的ルーティングと[param]の使い方

App Routerでも、Pages Routerと同様に動的ルーティングが可能です。ファイル名を角括弧で囲うことで定義します。

例:動的ルートの定義

tsx// app/blog/[id]/page.tsx

type Props = {
  params: { id: string }
};

export default function BlogPost({ params }: Props) {
  return <h1>ブログ記事 ID: {params.id}</h1>;
}

params.idによりURLパラメータが取得できます。getServerSidePropsgetStaticPropsは不要です。


Server ComponentとClient Componentの違いと使い分け

Next.js 13 App Routerでは、コンポーネントがサーバーで実行されるか、クライアントで実行されるかを明示的に区別する必要があります。

この考え方は、パフォーマンスやセキュリティ、レンダリング戦略に直結するため、非常に重要です。


Server Componentの特徴

Server Componentはデフォルトで有効です。つまり、特別な指定をしなければ、そのコンポーネントはサーバー側でレンダリングされます

利点

  • APIやDBに直接アクセス可能(Node.jsランタイムで動作)
  • 初回描画が高速(HTMLとして返却)
  • クライアントのバンドルサイズを削減可能

制約

  • useState, useEffectなどのクライアント専用Hooksは使用不可
  • ブラウザ専用のAPI(windowなど)も使用不可

例:Server Component

tsx// app/dashboard/page.tsx

async function getData() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
  return res.json();
}

export default async function DashboardPage() {
  const post = await getData();

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
}

このように、fetch()をそのまま使い、awaitでデータを取得しつつHTMLを返せます。


Client Componentの指定方法

Client Componentとして実行したい場合は、ファイルの先頭に次のように記述します。

tsx'use client';

用途

  • 状態管理(useState, useReducer
  • イベントハンドリング(ボタンクリックなど)
  • クライアントサイドのインタラクション

例:Client Componentのカウンター

tsx// app/components/Counter.tsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

このように、useStateなどのフックを使いたい場合は明示的にClient Componentにする必要があります。


Server Component × Client Componentの組み合わせ

App Routerでは、Server Component内にClient Componentを埋め込むことが可能です。

例:親はサーバー、子はクライアント

tsx// app/page.tsx(Server Component)

import Counter from './components/Counter';

export default function HomePage() {
  return (
    <div>
      <h1>ホームページ</h1>
      <Counter />
    </div>
  );
}

この構成により、非インタラクティブな部分はサーバーで高速に描画し、必要な箇所のみをクライアントで動的に扱うといった設計が可能になります。


metadataによるSEO設定の刷新

App Routerでは、<Head>タグを直接記述する代わりに、metadataオブジェクトを使ってSEO情報を設定します。

これは静的なメタ情報生成にも対応しており、SSR/SSGのどちらにも適応できる柔軟な方式です。


静的なmetadataの例

tsx// app/about/page.tsx

export const metadata = {
  title: 'Aboutページ | MySite',
  description: 'このページはMySiteのAboutページです。',
};

export default function AboutPage() {
  return <h1>About</h1>;
}

このようにmetadataをエクスポートするだけで、HTMLの<title><meta name="description">タグが生成されます。


動的にmetadataを生成する方法

動的ルートで記事タイトルなどを反映させたい場合は、generateMetadata()関数を使用します。

例:記事ごとのメタ情報設定

tsx// app/blog/[id]/page.tsx

export async function generateMetadata({ params }) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
  const post = await res.json();

  return {
    title: `${post.title} | ブログ`,
    description: post.body.slice(0, 100),
  };
}

詳細は公式の以下ドキュメントも併せてご覧ください: https://nextjs.org/docs/app/api-reference/functions/generate-metadata


generateStaticParamsによる静的生成(SSG)

App Routerでは、SSG対応としてgetStaticPathsの代わりにgenerateStaticParamsを使用します。

これにより、ビルド時に特定のパスで静的ページを生成できます。

例:/blog/1, /blog/2などを静的生成

tsx// app/blog/[id]/page.tsx

export async function generateStaticParams() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts = await res.json();

  return posts.slice(0, 5).map((post) => ({
    id: post.id.toString(),
  }));
}

これにより、該当するページは事前に生成され、表示が高速になります。

フォーム送信とAPI処理のパターン

App Routerでは、従来のAPI Routespages/api/)に加え、Server Actionsを活用することで、より直感的かつセキュアなフォーム送信処理が可能になります。

クライアントサイドのフォーム送信(fetch + API Routes)

従来通り、fetchを用いて/apiエンドポイントにPOSTする方式も利用可能です。

例:シンプルなフォーム送信(クライアント側)

tsx'use client';

import { useState } from 'react';

export default function ContactForm() {
  const [message, setMessage] = useState('');
  const [status, setStatus] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const res = await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify({ message }),
      headers: {
        'Content-Type': 'application/json',
      },
    });

    if (res.ok) {
      setStatus('送信されました');
    } else {
      setStatus('送信に失敗しました');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} />
      <button type="submit">送信</button>
      <p>{status}</p>
    </form>
  );
}

対応するAPIエンドポイント(pages/api/contact.ts)

ts// pages/api/contact.ts

export default function handler(req, res) {
  if (req.method === 'POST') {
    console.log('受信したメッセージ:', req.body.message);
    res.status(200).json({ status: 'ok' });
  } else {
    res.status(405).end(); // Method Not Allowed
  }
}

Server Actionsによるフォーム送信

Next.js 13では、server actionという新しい仕組みが導入されました。フォームに直接action属性として非同期関数を渡すことで、サーバーサイドで処理を行えます。

例:Server Actionsを使った送信処理

tsx// app/actions/submitMessage.ts

'use server';

export async function submitMessage(formData: FormData) {
  const message = formData.get('message')?.toString();
  console.log('送信内容:', message);
}
tsx// app/contact/page.tsx

import { submitMessage } from '../actions/submitMessage';

export default function ContactPage() {
  return (
    <form action={submitMessage}>
      <textarea name="message" />
      <button type="submit">送信</button>
    </form>
  );
}

この形式では、クライアントでfetchを書く必要がなく、サーバーで直接処理されるためセキュリティも高まります。

詳しくは公式ガイドも参考ください: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions


Pages RouterからApp Routerへの移行戦略

App Routerは新しく強力な仕組みですが、既存のプロジェクトを完全移行するのは大きな作業になります。

Next.jsではPages RouterとApp Routerの共存が可能なため、段階的な移行が現実的です。


共存可能な構成例

bash/app
  └─ page.tsx       → App Routerで構成

/pages
  └─ contact.tsx    → 従来のPages Routerがそのまま動作

どちらのルートも有効で、同名のルートがある場合は/appが優先されます。


段階的な移行手順の例

ステップ内容
1/appディレクトリを新規作成
2共通レイアウト部分(layout.tsx)を構築
3一部ページをapp/配下に移動
4metadataServer Componentを導入
5pages/ディレクトリの削除(完全移行)

App Routerでの認証処理のパターン

App Routerでの認証は、ミドルウェア・Server Component・useSessionフックなどを組み合わせて実装可能です。

例として、next-authをApp Routerで使用するケースをご紹介します。


認証ミドルウェアの導入

ts// middleware.ts

import { withAuth } from 'next-auth/middleware';

export default withAuth({
  pages: {
    signIn: '/login',
  },
});

export const config = {
  matcher: ['/dashboard/:path*'], // 保護したいルート
};

この設定により、/dashboard以下にアクセスする際はログインが必要になります。


サーバー側でのセッション取得

tsx// app/dashboard/page.tsx

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export default async function DashboardPage() {
  const session = await getServerSession(authOptions);

  if (!session) {
    return <p>ログインしてください</p>;
  }

  return <p>{session.user?.name}さん、ようこそ!</p>;
}

このように、App Routerでも柔軟な認証処理が実現可能です。


よくあるエラーとその対処方法

App Routerを導入するにあたって発生しやすいエラーと、その解決法をまとめます。

エラー内容原因と対処法
useStateを使ったらエラーになる'use client' が未指定。クライアントコンポーネントに明示指定
window is not defined エラーサーバー側で実行されている。Client Componentで処理する
childrenundefined になるlayout.tsx{children} を必ず受け取る構造にする
fetchのURLが不正localhost ではなく絶対URLかNext.jsのrevalidate戦略を検討

まとめ

Next.js 13のApp Routerは、Reactの最新仕様を活かした強力なルーティング機構です。

ディレクトリ構造によるルーティング定義、Server Componentsとの統合、レイアウトの柔軟な共有、metadataベースのSEO対策、Server Actionsによるフォーム送信など、現代的なWebアプリ構築に不可欠な機能が数多く取り入れられています。

移行は段階的に行えるため、既存のPages Routerプロジェクトを徐々に置き換えることも可能です。

ぜひこの機会にApp Routerの導入をご検討ください。

記事Article

もっと見る