Next.js App Router完全ガイド – Pages Routerからの移行と新機能の使い方

 

はじめに

Next.js 13で導入されたApp Routerは、React Server Componentsを活用した新しいルーティングシステムです。従来のPages Routerから大幅に改良され、より直感的で高性能なWebアプリケーション開発が可能になりました。この記事では、App Routerの基本概念から実践的な使い方まで詳しく解説します。

App Routerとは

App Router vs Pages Router

App Router(新方式)

  • React Server Components対応
  • ファイルベースルーティング(app ディレクトリ)
  • ネストされたレイアウト
  • 改善されたデータフェッチング

Pages Router(従来方式)

  • Client Side Rendering中心
  • pages ディレクトリベース
  • シンプルなルーティング
  • getServerSideProps/getStaticProps

基本的なディレクトリ構造

App Routerのファイル構成

app/
├── layout.js          # ルートレイアウト
├── page.js           # ホームページ
├── loading.js        # ローディングUI
├── error.js          # エラーページ
├── not-found.js      # 404ページ
└── blog/
    ├── layout.js     # ブログレイアウト
    ├── page.js       # ブログ一覧
    └── [slug]/
        └── page.js   # 個別記事

ルートレイアウトの作成

基本的なレイアウト

// app/layout.js
export const metadata = {
  title: 'My App',
  description: 'Generated by Next.js',
}

export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body>
        <header>ヘッダー</header>
        <main>{children}</main>
        <footer>フッター</footer>
      </body>
    </html>
  )
}

ネストされたレイアウト

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <nav>ブログナビゲーション</nav>
      <article>{children}</article>
    </div>
  )
}

ページコンポーネントの作成

基本的なページ

// app/page.js
export default function HomePage() {
  return (
    <div>
      <h1>ホームページ</h1>
      <p>App Routerで作成されたページです</p>
    </div>
  )
}

動的ルートの実装

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  return (
    <div>
      <h1>記事: {params.slug}</h1>
      <p>動的ルートで生成されたページです</p>
    </div>
  )
}

export async function generateStaticParams() {
  return [{ slug: 'first-post' }, { slug: 'second-post' }]
}

Server ComponentsとClient Components

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 (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

Client Component

// app/components/Counter.js
'use client'
import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

データフェッチングの新しい方法

fetch APIの拡張

// app/news/page.js
async function getNews() {
  const res = await fetch('https://api.news.com/articles', {
    next: { revalidate: 3600 } // 1時間でキャッシュ更新
  })
  return res.json()
}

export default async function NewsPage() {
  const news = await getNews()
  return (
    <div>
      {news.map(article => (
        <div key={article.id}>{article.title}</div>
      ))}
    </div>
  )
}

ISRの新しい書き方

// app/products/page.js
export const revalidate = 60 // 60秒でキャッシュ更新

async function getProducts() {
  const res = await fetch('https://api.shop.com/products')
  return res.json()
}

export default async function ProductsPage() {
  const products = await getProducts()
  return <div>{/* 商品一覧 */}</div>
}

特殊ファイルの活用

ローディングUI

// app/loading.js
export default function Loading() {
  return (
    <div className="flex justify-center items-center">
      <div className="spinner">読み込み中...</div>
    </div>
  )
}

エラーハンドリング

// app/error.js
'use client'

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>エラーが発生しました</h2>
      <button onClick={() => reset()}>再試行</button>
    </div>
  )
}

404ページ

// app/not-found.js
export default function NotFound() {
  return (
    <div>
      <h2>ページが見つかりません</h2>
      <p>お探しのページは存在しません</p>
    </div>
  )
}

メタデータの設定

静的メタデータ

// app/about/page.js
export const metadata = {
  title: 'About Us',
  description: '私たちについて',
}

export default function AboutPage() {
  return <div>About Us</div>
}

動的メタデータ

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  }
}

Route Handlersの使用

API Routes の新しい書き方

// app/api/users/route.js
export async function GET() {
  const users = await getUsers()
  return Response.json(users)
}

export async function POST(request) {
  const data = await request.json()
  const user = await createUser(data)
  return Response.json(user, { status: 201 })
}

ストリーミングとSuspense

Suspense境界の活用

// app/dashboard/page.js
import { Suspense } from 'react'

function SlowComponent() {
  // 時間のかかる処理
  return <div>重いコンポーネント</div>
}

export default function Dashboard() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <Suspense fallback={<div>読み込み中...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

Pages Routerからの移行

段階的移行の手順

// next.config.js(移行期間中の設定)
module.exports = {
  experimental: {
    appDir: true // App Routerを有効化
  }
}

移行チェックリスト

ファイル移行

  • pages → app ディレクトリ
  • _app.js → layout.js
  • _document.js → layout.js

データフェッチング移行

  • getServerSideProps → fetch with cache
  • getStaticProps → fetch with revalidate
  • getStaticPaths → generateStaticParams

パフォーマンス最適化

React Server Componentsの活用

// Server Component(高速)
async function ServerList() {
  const data = await fetchData()
  return (
    <ul>
      {data.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  )
}

// Client Component(必要な場合のみ)
'use client'
function InteractiveButton() {
  return <button onClick={handleClick}>クリック</button>
}

部分的プリレンダリング

// app/shop/page.js
export default function ShopPage() {
  return (
    <div>
      <StaticHeader /> {/* 静的部分 */}
      <Suspense fallback={<Loading />}>
        <DynamicProducts /> {/* 動的部分 */}
      </Suspense>
    </div>
  )
}

TypeScriptとの組み合わせ

型安全なページコンポーネント

// app/blog/[slug]/page.tsx
interface PageProps {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export default function BlogPost({ params, searchParams }: PageProps) {
  return <div>記事: {params.slug}</div>
}

実践的な使用例

eコマースサイトの構成

// app/shop/layout.js
export default function ShopLayout({ children }) {
  return (
    <div className="shop-layout">
      <aside>カテゴリー</aside>
      <main>{children}</main>
    </div>
  )
}

// app/shop/[category]/page.js
export default function CategoryPage({ params }) {
  return <div>カテゴリー: {params.category}</div>
}

よくある問題と解決策

Q: “use client”をいつ使う? A: useState、useEffect、イベントハンドラーを使う場合のみ

Q: Server Componentでエラーが出る A: ブラウザAPIの使用やuseStateがないか確認

Q: メタデータが反映されない A: page.jsファイル内でexportしているか確認

まとめ

Next.js App Routerは、React Server Componentsを活用した革新的なルーティングシステムです。従来のPages Routerと比べて、より高性能で開発者体験の優れたアプリケーション開発が可能になります。

段階的な移行を行い、新機能を活用することで、現代的なWebアプリケーションを効率的に構築できます。Server ComponentsとClient Componentsを適切に使い分け、ストリーミングやSuspenseを活用して、ユーザーにとって快適な体験を提供しましょう。


この記事はNext.js 13+のApp Routerに基づいて作成されています。最新の機能については公式ドキュメントもご確認ください。

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座