お疲れさまです、テックバディ運営の堀江です。AI駆動開発を学べるエンジニア向けスクール「テックバディ」で教材を作っていると、まさに今回のテーマで手が止まる受講生に何度も出会いました。
「Next.jsのチュートリアルはPages Routerで書かれているのに、現場のコードはApp Router」——これで一度は混乱したことがあるのではないでしょうか。ネットの記事も新旧の情報が混在していて、どちらの書き方を信じればいいか分からなくなりがちです。
移行期特有の情報の少なさもあり、ここでつまずきポイントを整理しておきます。
そもそも何が違うのか
一番の違いは「ファイルの役割」です。
| 観点 | Pages Router | App Router |
|---|---|---|
| ディレクトリ | pages/ |
app/ |
| ルーティングの単位 | ファイル1つ=1ページ | フォルダ1つ=1ルート |
| デフォルトのレンダリング | クライアントコンポーネント | サーバーコンポーネント |
| データ取得 |
getServerSideProps等の専用関数 |
コンポーネント内で直接fetch
|
| レイアウト共有 |
_app.tsxで一括管理 |
layout.tsxをネストして管理 |
以下、つまずきやすい5つのポイントを具体的に見ていきます。
つまずき1:ファイルが「ページ」ではなく「フォルダ」になる
Pages Routerではpages/about.tsxが1ページでしたが、App Routerではapp/about/page.tsxのようにフォルダの中にpage.tsxを置く形になります。
# Pages Router
pages/
about.tsx → /about
# App Router
app/
about/
page.tsx → /about
慣れないうちは「なぜファイルじゃなくてフォルダなのか」と戸惑いますが、これはフォルダの中にlayout.tsxやloading.tsxなど役割ごとのファイルを並べて置けるようにするための設計です。
つまずき2:デフォルトがサーバーコンポーネントになった
App Routerでは、何も指定しなければコンポーネントはサーバー上で実行されます。useStateやonClickなどブラウザ側の機能を使いたい場合は、ファイル先頭に明示的な宣言が必要です。
// これはエラーになる(サーバーコンポーネントでuseStateは使えない)
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// 先頭に'use client'を付けて初めてクライアントコンポーネントになる
'use client';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
'use client'を付け忘れると「useStateが使えません」というエラーが出るので、まずここを疑いましょう。逆に何でもかんでも'use client'を付けてしまうと、App Routerの利点(サーバー側で描画してJSを減らす)が失われるので、本当にブラウザ側の機能が必要な箇所だけに絞るのがポイントです。
つまずき3:データ取得の書き方がまるごと変わった
Pages RouterではgetServerSidePropsやgetStaticPropsという専用関数でデータを取っていましたが、App Routerではコンポーネントの中で直接fetchをawaitできます。
// Pages Router
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return { props: { data } };
}
// App Router(サーバーコンポーネントの中でそのままawait)
export default async function Page() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return <div>{data.title}</div>;
}
コンポーネント自体をasync functionにできる、という点が最初は不思議に見えますが、サーバーコンポーネントだからこそ許される書き方です。
つまずき4:レイアウトの共有方法が変わった
Pages Routerで全ページ共通のヘッダー・フッターを_app.tsxにまとめて書いていた人は、App Routerのlayout.tsxのネスト構造に戸惑いやすいです。
app/
layout.tsx → 全ページ共通のレイアウト
blog/
layout.tsx → /blog配下だけの追加レイアウト
[slug]/
page.tsx → /blog/xxx の中身
layout.tsxはフォルダごとに追加でき、下の階層のレイアウトは上の階層のレイアウトに包まれる形で表示されます。「どのレイアウトがどのページに効いているか」が分からなくなったら、フォルダ階層を上から順に辿って確認しましょう。
つまずき5:ルーティングの動的パラメータの受け取り方
[id]のような動的ルートで、パラメータの受け取り方も変わっています。
// Pages Router
export default function Post({ params }) {
const router = useRouter();
const { id } = router.query;
}
// App Router
export default function Post({ params }) {
const { id } = params; // propsとして直接渡ってくる
}
useRouterから取っていた情報が、App Routerでは素直にpropsとして渡ってくる分シンプルですが、「なぜuseRouterを使わなくていいのか」で一度混乱するポイントです。
まとめ
App RouterはPages Routerの単純な置き換えではなく、「サーバーで描画することを前提にした設計」への転換です。つまずいたら、まず「これはサーバー側の話か、クライアント側の話か」を切り分けて考えると、公式ドキュメントも読みやすくなります。新旧の情報が混在している今だからこそ、どちらの書き方を見ているか意識しながら学習するのがおすすめです。
AI駆動開発を実務レベルで学べるエンジニア向けスクール「テックバディ」について
「テックバディ」は、生成AIを使いこなして実務レベルの開発をやりきる力を、模擬案件を通じて身につけるエンジニア向けスクールです。「AIに書かせるだけ」で終わらせず、設計力・品質担保力・判断力まで鍛えるカリキュラムで、現場で任される仕事の幅を広げたい方をサポートしています。