Next.js Page Routerを App Routerへ
はじめに
最近、自社プロジェクトでNext.jsのPageRouterからAppRouterへの移行作業に参加しました。
Next.js13から導入されたAppRouterは、従来のPageRouterと比べて構成やデータ取得の考え方が大きく異なり、最初は戸惑う点も多くありました。
本記事では、実際の移行プロジェクトで得た経験をもとに、以下のような内容を整理していきます。
- PageRouterとAppRouterの違い
- 移行の手順と注意点
- ハマったポイントとその解決策
これからAppRouterを使ってみたい方や、既存プロジェクトの移行を検討している方の参考になれば幸いです。
PageRouterの特徴
Next.jsのPageRouterは、pages/ディレクトリにファイルを配置するだけで自動的にルーティング構成がされます。
実際、今回の移行前の自社プロジェクトでもこの方式で構成されてました。
特徴
-
pages/index.tsxでトップページが作成され、URL/に自動的に割り当てられる -
pages/about.tsx→/aboutのように、ファイル構成=ルーティング構成 -
getStaticProps,getServerSidePropsによるデータ取得がページ単位で可能 -
_app.tsx,_document.tsxを使って全体のラップやメタ情報の制御ができる - APIルート(pages/api/)も同じディレクトリ構造で定義可能
App Routerの特徴
Next.js13から正式に導入されたAppRouterは、それぞれのPageRouterに変わる新しいルーティングシステムです。app/ディレクトリをルートとし、ReactServerComponentsとの統合を前提とした、より柔軟かつモダンな設計になっています。
特徴
-
app/ディレクトリをルートに、URL構成と一致するようにディレクトリ&ファイルを構成 - 各ページは
page.tsx、共通レイアウトはlayout.tsxで定義 - ローディング状態を制御する
loading.tsxエラーを扱うerror.tsxなどもページ単位で用意可能 - クライアント・サーバーの責務を分けるための
use clientディレクティブが導入された -
useRouterの代わりにusePathnameやuseSearchParamsなどの新しいフックが登場(next/navigation)
AppRouterのディレクトリ構成例
app/
├── layout.tsx ← 全体レイアウト(ヘッダー・フッターなど)
├── page.tsx ← トップページ
├── about/
│ ├── layout.tsx ← /about 配下のレイアウト
│ ├── page.tsx ← /about ページ
│ └── loading.tsx ← /about のローディングUI
├── contact/
│ ├── page.tsx ← /contact ページ
├── api/
│ └── hello/route.ts ← API ルート(/api/hello)
各ファイルの役割
-
layout.tsx:各階層の共通レイアウト -
page.tsx:各ページコンテンツ本体(URLに対応) -
loading.tsx:非同期読み込み中のプレースフォルダーUI -
error.tsx:ページ単位のエラーハンドリング -
route.ts:AppRouterでのAPIエンドポイント(GET、POSTなど記述)
PageRouter から AppRouter への移行手順
-
app/ディレクトリの作成と基本構成の整備
まずは、既存のpages/フォルダとは別にapp/ディレクトリを作成し、以下の基本ファイルを用意。
app/
├── layout.tsx // 全体レイアウト
├── page.tsx // トップページ(`pages/index.tsx`の置き換え)
layout.tsxの初期構成例
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>{children}</body>
</html>
);
}
この段階では、app/ディレクトリ内のページのみAppRouterのルールが適用されます。
-
_app.tsxの役割をlayout.tsxに移行
AppRouterでは_app.tsxのようなエントリーポイントは存在せず、代わりにlayout.tsxでアプリ全体のレイアウトや共通処理を定義します。
// app/layout.tsx
import './globals.css';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
3.ページ単位でpage.tsxに置き換え
PageRouterのpages/ディレクトリ内の各ページファイルを、app/配下の対応するパスにpage.tsxとして移行します。
4.データ取得方法を変更
AppRouterでは、getStaticPropsやgetServerSidePropsは使用できません。代わりに、サーバーコンポーネント内でのasync関数や、fetchAPIを使って直接データを取得します。
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://example.com/api/posts');
return res.json();
}
export default async function PostPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
また、クライアント側で状態管理やデータ取得をしたい場合は、"use client"を宣言したクライアントコンポーネント内でuseEffectなどを使います。
ハマったポイントとその解決策
AppRouterへの移行で一見スムーズに見えても、実際の開発ではいくつかのハマりポイントがありました。特に実際の移行時に遭遇したトラブル、解決方法をご紹介します。
1.Linkのhrefにオブジェクトを渡すと動かない(pathpida使用時)
💥 問題
PageRouter時代に使っていたpathpidaで、以下のように$url()の戻り値をそのままLinkのhrefに渡していた
<Link href={pagesPath.login.reminder.$url()}>
パスワードリセット
</Link>
AppRouterに移行後、この記述で遷移しようとすると、URLが[object Object]になるなど、リンクが壊れる現象が発生。
✅ 解決策
AppRouterでは、Linkのhrefにオブジェクト形式はサポートされておらず、文字列を直接渡す必要があります。
そのため、以下のように.pathnameや.pathを明示的に使うことで回避できます。
<Link href={pagesPath.login.reminder.$url().pathname}>
パスワードリセット
</Link>
💡 補足
この問題は静的ルート・動的ルート問わず発生します。AppRouterでは、href="/some/path"のようなリテラル形式のリンクが基本になるため、pathpidaを使用する場合は.pathnameを常に取り出して渡すようにルール化するのが安全のようです。
2.use clientの書き忘れでエラー
💥 問題
AppRouterではデフォルトがサーバーサイドコンポーネントになっているため、状態管理やイベントハンドラを含む処理をそのまま書くと実行エラーになる。
// エラーになる例(useStateが使えない)
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0); // サーバーコンポーネントでは使えない
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
✅ 解決策
クライアントコンポーネントとして明示するために、冒頭に "use client" を記述する必要があります。
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
3.useRouterが使えない・移行漏れ
💥 問題
Page Routerで使用していたnext/routerのuseRouter()フックは、AppRouterでは非推奨です。usePathname, useSearchParamsがnext/navigationから提供されています。
✅ 解決策
AppRouterでは、以下のように書き換える必要があります。
//PageRouter
import { useRouter } from 'next/router';
const router = useRouter();
router.push('/about');
//AppRouter
'use client';
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/about');
pathnameやクエリの取得も以下のように変更されます。
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname();
const searchParams = useSearchParams();
まとめ
AppRouterへの移行では、見た目の構成変更だけでなく、Linkの扱いやデータ取得方法、クライアント/サーバーコンポーネントの責務分離など、根本的なアーキテクチャの変化に注意が必要です。
特に既存プロジェクトでpathpidaやuseRouterなどを活用していた場合、挙動の違いや非互換が思わぬバグの原因になることがあります。
この記事が、同じようにAppRouterへの移行を考えている方の参考になれば幸いです。
Discussion