言語を切り替える
テーマを切り替える

Next.js App Router 入門ガイド:コア概念と基本操作を解説

Next.js の公式ドキュメントを初めて開いたとき、正直ボーっとしました。左サイドバーに「Pages Router」と「App Router」が並んでいて、まるで「好きな方を選んで」と言われているよう。でも肝心の「どっちを選ぶ?」「何が違う?」という答えは見当たらず、読めば読むほど混乱する。チュートリアルによっては pages フォルダ、あるいは app フォルダを使っていて、コードの書き方もまったく違います。

後から分かったのですが、Next.js にはまったく別の 2 つのルーティングシステムがあります。古い Pages Router は安定していますが、一部の新機能は使えません。新しい App Router は v13 で登場し、v13.4 で安定版。今や公式推奨の方向です。

「それでも App Router を学ぶ必要がある?」「また面倒な新しいものなのでは?」——そう思うかもしれません。

この記事は、そんな迷いを解消するために書きました。Server Components とは何か、特殊ファイルの使い方、Pages Router との違いを、できるだけシンプルに整理します。読み終えれば、無駄な回り道を減らして App Router に着手できます。

App Router とは? なぜ使うのか?

一言でいうと、App Router は Next.js v13 で導入された新ルーティングシステムです。React の最新機能 Server Components(サーバーコンポーネント)を土台に、よりモダンで柔軟な設計になっています。

Pages Router と比べて、目に見えるメリットは 3 つ。

1. パフォーマンスが向上
App Router はデフォルトでサーバーコンポーネント。大部分のコードがサーバーで動くため、ブラウザがダウンロードする JavaScript が減り、ページ表示が速くなります。Vercel の 2024 年レポートによると、トップクラスの Next.js アプリの 60% 以上が App Router に移行済みです。

"トップクラスの Next.js アプリの 60% 以上が App Router に移行済み"

2. レイアウトが柔軟
Pages Router ではネストレイアウトの実装が面倒でした。App Router なら layout.js 1 ファイルで済み、ページ切り替え時にレイアウトが再レンダリングされない——操作感が滑らかです。

3. エラー処理とロード状態が強力
loading.js でローディング UI、error.js でエラーを捕まえてフォールバック UI を表示。Pages Router では自前実装が必要だった部分が、App Router では規約として用意されています。

Pages Router はまだ使える? はい、使えます。
2 系統は共存できます。ただ、今から Next.js を学ぶなら App Router 一択。公式推奨の方向であり、v14.1.4 以降の新規プロジェクトのスキャフォールドもデフォルトで App Router です。

ファイルシステムルーティング:フォルダからページへ

App Router の根幹は、フォルダ構造がそのままルート構造になるということ。

抽象論はここまで。例を見ましょう:

app/
├── page.js              # トップページ → /
├── about/
│   └── page.js          # About → /about
└── blog/
    ├── page.js          # ブログ一覧 → /blog
    └── [slug]/
        └── page.js      # 記事詳細 → /blog/:slug

覚えておきたいポイントは 3 つ。

1. page.js がルートの入口
page.js という名前のファイルだけが公開ページになります。layout.jsloading.js などは機能ファイルで、直接 URL にはなりません。

2. 動的ルートは角括弧
/blog/hello-world のような動的ルートは app/blog/[slug]/page.js を作れば OK。パラメータ slug がコンポーネントに渡されます:

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  return <h1>記事:{params.slug}</h1>
}

3. キャッチオールは [...slug]
/docs/a/b/c のように多段パスを扱うなら app/docs/[...slug]/page.jsparams.slug['a', 'b', 'c'] という配列になります。

Pages Router との比較
Pages Router では pages/blog/[id].js でした。App Router では app/blog/[id]/page.js と、フォルダが 1 段増えます。各ルートに layout.jsloading.js を置くスペースを確保するためです。

最初は手間に感じるかもしれません。慣れるとプロジェクト構造の見通しが一気に良くなります。

Server Components vs Client Components:コア概念

App Router で最も混乱しやすいテーマ。私も最初はかなり迷いました。

要点は 1 行。App Router のコンポーネントはデフォルトでサーバー実行。インタラクションが必要なときだけブラウザで動く。

デフォルトは Server Component

app/ 配下のコンポーネントは、すべて Server Component(サーバーコンポーネント)がデフォルト。サーバーでレンダリングした HTML をブラウザに送ります。

メリットは明確です:

  • JavaScript 量が少ない:コードをブラウザに送らないので、ダウンロードする JS が小さくなる
  • バックエンドに直接アクセス:DB クエリや API キーなど機密情報を安全に扱える
  • 初回表示が速い:サーバーで描画済み HTML が届くので FCP(First Contentful Paint)が短い

典型的な Server Component の例:

// app/products/page.js
// Server Component — サーバー側で実行
async function getProducts() {
  const res = await fetch('https://api.example.com/products')
  return res.json()
}

export default async function ProductsPage() {
  const products = await getProducts()

  return (
    <div>
      <h1>製品一覧</h1>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  )
}

useEffectgetServerSideProps も不要。コンポーネント内で async/await するだけ。

いつ Client Component を使う?

次のような場面ではブラウザ実行が必要です:

  • React hooks(useStateuseEffect)を使う
  • ユーザー操作(onClickonChange)を扱う
  • ブラウザ API(localStoragewindow)を使う

そのときは Client Component(クライアントコンポーネント)。ファイル先頭に 'use client' を 1 行追加するだけ:

// components/AddToCartButton.js
'use client'  // Client Component として宣言

import { useState } from 'react'

export default function AddToCartButton({ productId }) {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      カートに追加 ({count})
    </button>
  )
}

混在利用:ベストプラクティス

Server Component と Client Component は組み合わせられます。

製品ページの例:

  • 製品リスト → Server Component(サーバーでデータ取得、JS 削減)
  • カート追加ボタン → Client Component(クリック処理が必要)
// app/products/page.js (Server Component)
import AddToCartButton from '@/components/AddToCartButton' // Client Component

async function getProducts() {
  // サーバー側でデータ取得
}

export default async function ProductsPage() {
  const products = await getProducts()

  return (
    <div>
      <h1>製品一覧</h1>
      {products.map(p => (
        <div key={p.id}>
          {p.name}
          <AddToCartButton productId={p.id} />
        </div>
      ))}
    </div>
  )
}

原則:デフォルトは Server Component。 本当にインタラクションが要るときだけ 'use client'

全コンポーネントに 'use client' を付けるのは本末転倒——App Router を使う意味が薄れます。

特殊ファイル:プロジェクトを一段上に

App Router には layout.jsloading.jserror.js など、特殊なファイル名が定義されています。最初は面倒に見えますが、使うと手放せません。

layout.js:共有レイアウト

最もよく使う特殊ファイル。ルートセグメントのレイアウトを定義し、同階層・子階層のページを包みます。

アプリ全体にナビとフッターを付ける例:

// app/layout.js (ルートレイアウト)
export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body>
        <nav>ナビゲーション</nav>
        <main>{children}</main>
        <footer>フッター</footer>
      </body>
    </html>
  )
}

レイアウトのネストも可能:

app/
├── layout.js          # グローバル(ナビ+フッター)
├── page.js            # トップページ
└── dashboard/
    ├── layout.js      # ダッシュボード(サイドバー)
    ├── page.js        # /dashboard
    └── settings/
        └── page.js    # /dashboard/settings

/dashboard から /dashboard/settings へ移動しても、グローバルとダッシュボードのレイアウトは再レンダリングされず、page.js だけ更新。ナビがちらつかない理由はここにあります。

loading.js:ロード状態

useState でローディングを管理する必要はありません。loading.js を置けば、App Router が自動で Suspense でページを包みます:

// app/dashboard/loading.js
export default function Loading() {
  return <div>読み込み中...</div>
}

データ取得中は loading.js の内容が表示される——それだけです。

error.js:エラー境界

ページのエラーを捕まえ、フォールバック UI を出すファイル:

// app/dashboard/error.js
'use client'  // Error Boundary は Client Component 必須

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>エラー:{error.message}</h2>
      <button onClick={reset}>再試行</button>
    </div>
  )
}

落とし穴error.js は同階層の layout.js のエラーを捕まえられません。React Error Boundary は子コンポーネントのエラーしか対象外だからです。

レイアウトのエラーは、親ディレクトリの error.js か、ルートの global-error.js で扱います。

not-found.js:404 ページ

存在しないルート向け:

// app/not-found.js
export default function NotFound() {
  return <h1>ページが見つかりません</h1>
}

コードから 404 を明示的に発火することもできます:

import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  if (!post) notFound()  // not-found.js を表示

  return <article>{post.title}</article>
}

ファイルの階層関係

特殊ファイルには決まった階層があります:

layout.js
├── loading.js  (Suspense 境界)
│   └── page.js
└── error.js    (Error 境界)

layout が最外層で、error.js はそれを包めません。loading.js がロード、error.js がエラー担当。

この構造を頭に入れておけば、よくあるハマりどころを避けられます。

データ取得:getServerSideProps からの卒業

Pages Router を使ったことがあるなら、getServerSidePropsgetStaticProps を書いたはず。関数を別途 export する API は、データの流れも直感的ではありませんでした。

App Router はここを大幅に簡素化します。

async/await をそのまま使う

Server Component 内で、コンポーネント関数から直接データ取得:

// app/posts/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

特別な API は不要。普通の async/await です。

並列データ取得

複数ソースを並列で取れます:

export default async function Dashboard() {
  // 並列取得 — 互いをブロックしない
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats()
  ])

  return (
    <div>
      <h1>{user.name}</h1>
      <Posts data={posts} />
      <Stats data={stats} />
    </div>
  )
}

キャッシュと再検証

Next.js は fetch を自動キャッシュ。戦略はオプションで制御:

// 60 秒キャッシュ後に再検証
fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// キャッシュなし — 毎回最新
fetch('https://api.example.com/data', {
  cache: 'no-store'
})

Pages Router との比較

  • Pages Router:getServerSideProps + getStaticProps、関数を個別 export
  • App Router:コンポーネント内で async/await

シンプルになりましたね。

初心者がよくぶつかる問題と対処

App Router を学ぶとき、私もいくつかハマりました。よくある 5 つをまとめます。

問題 1:いつ ‘use client’ を付ける?

迷いどころ:チュートリアルに 'use client' が多く、付けるタイミングが分からない。

対処
原則は デフォルトでは付けない。必要なときだけ付ける。

'use client' が要るのは次の場合だけ:

  • React hooks(useStateuseEffectuseContext
  • ユーザー操作(onClickonChange
  • ブラウザ API(windowlocalStorage

それ以外は付けない。Server Component のほうが速く、バックエンドにも直接触れます。

問題 2:layout.js と page.js の関係は?

迷いどころ:同じフォルダに 2 ファイルがある。どちらがどちらを包む?

対処
layout.jspage.js と子ルートを包みます。

app/
├── layout.js       # 以下すべてのページを包む
├── page.js         # トップ — 上の layout に包まれる
└── about/
    └── page.js     # About — これも上の layout に包まれる

ページ切り替え時、layout.js は再レンダリングされず page.js だけ更新。ナビがちらつかないのはこの仕組みです。

問題 3:動的ルートのパラメータはどう取る?

迷いどころ[slug]/page.js を作ったが、slug の値が取れない。

対処
params プロパティから取得:

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  console.log(params.slug)  // URL の slug 値
  return <h1>記事:{params.slug}</h1>
}

ネスト動的ルート app/blog/[category]/[slug]/page.js の場合:

export default function Post({ params }) {
  console.log(params.category, params.slug)
  return <h1>{params.category} - {params.slug}</h1>
}

問題 4:error.js が効かない?

迷いどころerror.js を置いたのに、レイアウトのエラーが捕まらない。

対処
error.js は同階層 layout.js のエラーを捕まえられません。Error Boundary の制限です。

レイアウトエラーの対処は 2 通り:

  1. 親ディレクトリに error.js を置く
  2. ルートに global-error.js<html><body> を含める)
// app/global-error.js
'use client'

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>グローバルエラー:{error.message}</h2>
        <button onClick={reset}>再試行</button>
      </body>
    </html>
  )
}

問題 5:既存プロジェクトは移行すべき?

迷いどころ:App Router の新機能を見て、全部書き直す必要があるのでは、と不安。

対処
焦る必要はありません。

Pages Router と App Router は共存できます:

  • 既存機能 → pages/ のまま
  • 新機能 → app/ で追加

Vercel 公式も Pages Router の長期サポートを表明しています。

新規プロジェクトなら App Router 一択。エコシステムも今後さらに充実していきます。

まとめ

App Router の 5 つのコア概念を振り返りましょう:

  1. ファイルシステムルーティング:フォルダ構造=ルート構造。入口は page.js
  2. Server Components:デフォルトでサーバー実行。パフォーマンスに有利
  3. Client Components:インタラクション時に 'use client'
  4. 特殊ファイルlayout.jsloading.jserror.js でプロ品質の UX
  5. データ取得async/await を直接使い、getServerSideProps から卒業

App Router は Next.js の未来。Vercel もコミュニティも積極的に投資しています。今から学ぶなら、App Router から始めて間違いありません。

次のステップは?

手を動かす。 小さなプロジェクトを作り、App Router でブログや Todo アプリを試してみてください。概念を読むより、一度コードを書いたほうが早い。

詰まっても慌てないで。Pages Router との共存が可能です。どうしても難しければ、まず Pages Router で進め、徐々に移行すれば OK。

Next.js 公式ドキュメントは散らばっていますが、App Router 部分は比較的丁寧です。具体的な問題はドキュメントや GitHub Discussions を当たってみてください。

学習がうまくいくことを願っています!

FAQ

いつ 'use client' を使うべきか?
インタラクションが必要なときだけ。React hooks(useState、useEffect)、ユーザー操作(onClick、onChange)、ブラウザ API(window、localStorage)を使う場合。デフォルトは Server Component のほうがパフォーマンスに有利。
layout.js と page.js の関係は?
layout.js が page.js と子ルートを包みます。ページ切り替え時、layout.js は再レンダリングされず page.js だけ更新されるため、ナビバーなど共有 UI がちらつきません。
動的ルートパラメータはどう取得する?
コンポーネントの params プロパティから取得します。例:app/blog/[slug]/page.js では { params } を受け取り、params.slug で URL の slug 値にアクセスします。
error.js が layout.js のエラーを捕まえられないのはなぜ?
React Error Boundary の制限です。子コンポーネントのエラーしか捕まえられず、同階層や親のエラーは対象外。レイアウトエラーは親ディレクトリの error.js か、ルートの global-error.js で扱います。
既存プロジェクトを App Router に移行すべき?
急ぐ必要はありません。Pages Router と App Router は共存でき、旧機能は pages/、新機能は app/ で進められます。Vercel 公式も Pages Router の長期サポートを表明しています。新規プロジェクトは App Router 推奨です。
App Router と Pages Router の主な違いは?
App Router は Server Components ベースで、デフォルトがサーバーサイドレンダリング。ファイルシステムルーティングとネストレイアウトに対応し、データ取得は async/await を直接使えます。Pages Router は主にクライアントコンポーネントで、getServerSideProps が必要です。

4分で読めます · 公開日: 2025年12月18日 · 更新日: 2026年6月8日

シリーズの読書導線 第 1 / 47 記事

Next.js 完全ガイド

このページはシリーズの最初の記事です。次の記事へ進むか、シリーズ全体ページで全体像を確認できます。

シリーズ全体を見る

関連記事

コメント

GitHubアカウントでログインしてコメントできます