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

Next.js App Router 入門ガイド:コアコンセプトと基本的な使い方を完全解説

はじめに

正直なところ、私が初めて Next.js の公式ドキュメントを開いたときは困惑しました。

左側のサイドバーに「Pages Router」と「App Router」が並んでいて、「好きな方を選んでね」と言わんばかりです。でも問題は――どっちを選べばいいの? 2つの違いは何? どっちを学ぶべき? ドキュメントは直接的な答えをくれず、見れば見るほど混乱しました。チュートリアルによっては pages フォルダーを使っていたり、app フォルダーを使っていたり、コードの書き方も全く違います。

その後、ようやく理解しました。Next.js には実は全く異なる2つのルーティングシステムがあるのです。古い方が Pages Router で、安定していて信頼できますが、いくつかの新機能は使えません。新しい方が App Router で、v13 から導入され、v13.4 で正式に安定版となり、現在は公式に推奨される方向性となっています。

「じゃあ、App Router を学ぶべきなの? また新しいものを覚えるのは面倒じゃない?」と思うかもしれません。

この記事は、そんな疑問を解消するために書きました。App Router のコアコンセプト――Server Components とは何か、特殊ファイルの使い方はどうするか、Pages 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 ファイルを使うだけで簡単に実現でき、ページ切り替え時にレイアウトが再レンダリングされないため、体験がスムーズです。

3. 強力なエラー処理とロード状態
loading.js ファイルでロード中のアニメーションを定義し、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 に対応

いくつか重要なポイントがあります:

1. page.js がルーティングのエントリポイント
page.js という名前のファイルだけが、アクセス可能なページになります。その他のファイル(例えば layout.jsloading.js)は関連機能ファイルであり、直接アクセスされることはありません。

2. 動的ルーティングは角括弧を使用
/blog/hello-world のような動的ルートが必要ですか? app/blog/[slug]/page.js を作成すれば、パラメータ 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.js を使えば、params.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 で最も混乱しやすい概念です。私が学び始めたときも、しばらく混乱していました。

一言で言えば、App Router 内のコンポーネントはデフォルトでサーバー側で実行され、対話性が必要な場合のみブラウザ側で実行されます

デフォルトは Server Component

app/ ディレクトリ内に作成されたコンポーネントは、デフォルトですべて Server Component(サーバーコンポーネント)です。これらはサーバー側でレンダリングが完了し、HTML をブラウザに直接送信します。

メリットは明らかです:

  • JavaScript サイズが小さい:コードをブラウザに送信する必要がないため、ユーザーがダウンロードする JS ファイルが小さくなります。
  • バックエンドリソースに直接アクセス可能:データベースクエリや API キーなどの機密情報を安全に使用できます。
  • 初回表示が速い:サーバーでレンダリング済みのものが送られてくるため、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(useState, useEffect)を使用する場合
  • ユーザーインタラクション(onClick, onChange)を処理する場合
  • ブラウザ API(localStorage, window)を使用する場合

この場合、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>
  )
}

混合使用:ベストプラクティス

本当に素晴らしいのは、これら2種類のコンポーネントを混ぜて使えることです。

例えば、ある製品ページでは:

  • 製品リストは 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.js, loading.js, error.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 boundaries は 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 の制限として、子コンポーネントのエラーのみをキャッチでき、自分自身や親のエラーはキャッチできないからです。

layout.js のエラーをキャッチするには、親ディレクトリに 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 Boundary)
│   └── page.js
└── error.js    (Error Boundary)

layout が一番外側にあり、error.js はそれを包めません。loading.js は読み込み状態を担当し、error.js はエラー処理を担当します。

この階層を理解しておけば、落とし穴にはまることはありません。

データフェッチ:getServerSideProps との別れ

Pages Router を使っていたなら、getServerSidePropsgetStaticProps を書いたことがあるはずです。正直なところ、あの 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>
  )
}

ご覧の通り、普通の async/await です。特別な API は必要ありません。

並列データフェッチ

さらに強力なのは、複数のデータソースを並列で取得できることです:

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、関数を個別にエクスポート
  • App Router:直接 async/await、コンポーネント内でデータ取得

ずっとシンプルになりましたよね?

初心者がよく遭遇する問題と解決策

App Router を学ぶ際、私は多くの落とし穴にはまりました。ここで最も一般的な問題をいくつかまとめましたので、参考にしてください。

問題1:いつ ‘use client’ を使うべきか?

混乱点:チュートリアルの至る所に 'use client' があり、いつ追加すべきかわからない。

解決策
原則を覚えてください――デフォルトでは追加せず、必要な時だけ追加する

以下の状況でのみ 'use client' を追加します:

  • React hooks(useState, useEffect, useContext)を使用する
  • ユーザーインタラクション(onClick, onChange)がある
  • ブラウザ API(window, localStorage)を使用する

それ以外は追加しないでください。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 内のその値
  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 のエラーをキャッチできません。これは React 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.js, loading.js, error.js でプロジェクトをよりプロフェッショナルにします。
  5. データフェッチ:直接 async/await を使い、getServerSideProps とはお別れです。

App Router は間違いなく Next.js の未来です。Vercel は継続的に投資しており、コミュニティも積極的に追随しています。今 Next.js を学び始めるなら、App Router から始めて間違いありません。

次はどうしますか?

手を動かしてみましょう。小さなプロジェクトを作成し、App Router を使ってブログや Todo アプリを作ってみてください。概念をどれだけ読むよりも、自分でコードを一度書く方がはるかに理解できます。

問題にぶつかっても慌てないでください。Pages Router と App Router は共存できます。どうしても解決できない場合はとりあえず Pages Router を使い、徐々に移行すればいいのです。

最後に、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 のみが更新されるため、ナビゲーションバーなどの共有要素がちらつきません。
動的ルートパラメータを取得するには?
コンポーネントの 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 が必要です。

6 min read · 公開日: 2025年12月18日 · 更新日: 2026年1月22日

コメント

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

関連記事