Next.js 16 App Router入門 - page・layout・動的ルート

初級 | 20分 で読める | 2024.12.17

公式ドキュメント

今回やること

Next.js 16のApp Routerで、小さな記事一覧を作ります。

  • layout.tsxで全ページ共通のナビゲーションを置く
  • page.tsxでURLに対応する画面を作る
  • [slug]で動的ルートを作る
  • <Link>でページ間を移動する
  • 状態が必要な小部品だけをClient Componentにする

外部APIやデータベースは使いません。ルーティングとServer / Client Componentの境界に集中します。

前提条件

  • Node.js 20.9以上
  • Reactのコンポーネントとpropsが分かる
  • TypeScriptの配列とオブジェクトが読める
node --version
npm --version

プロジェクトを準備する

npx create-next-app@16 app-router-practice \
  --typescript \
  --eslint \
  --app \
  --no-src-dir \
  --no-tailwind \
  --use-npm \
  --yes
cd app-router-practice

@16でNext.jsのmajor versionを16に固定しています。作成後にnpm list nextを実行し、next@16.xと表示されることを確認してください。この記事では、ルート直下のappディレクトリを使います。

app/
├── layout.tsx
├── page.tsx
├── post-data.ts
├── ui/
│   └── counter.tsx
└── posts/
    └── [slug]/
        ├── page.tsx
        └── not-found.tsx

共通のlayoutを作る

app/layout.tsxを次の内容にします。

import type { Metadata } from "next";
import Link from "next/link";
import type { ReactNode } from "react";
import "./globals.css";

export const metadata: Metadata = {
  title: "App Router練習",
  description: "Next.js 16のルーティング練習",
};

export default function RootLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <html lang="ja">
      <body>
        <header>
          <nav>
            <Link href="/">記事一覧</Link>
          </nav>
        </header>
        <main>{children}</main>
      </body>
    </html>
  );
}

layout.tsxは配下のページで共有されます。childrenの位置に、現在のURLに対応するページが入ります。

ローカルの記事データを作る

app/post-data.tsを作ります。

export const posts = [
  {
    slug: "first-post",
    title: "最初の記事",
    body: "page.tsxがURLに対応する画面を作ります。",
  },
  {
    slug: "server-component",
    title: "Server Component",
    body: "App Routerのpageとlayoutは標準でServer Componentです。",
  },
];

Client Componentを作る

app/ui/counter.tsxを作ります。

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount((value) => value + 1)}>
      読んだ回数: {count}
    </button>
  );
}

useStateとクリック処理はブラウザ側の状態を使うため、ファイル先頭に"use client"が必要です。大きなページ全体ではなく、対話操作が必要な小部品だけをClient Componentにします。

一覧pageを作る

app/page.tsxを次の内容にします。

import Link from "next/link";
import { posts } from "./post-data";
import Counter from "./ui/counter";

export default function HomePage() {
  return (
    <>
      <h1>記事一覧</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link href={`/posts/${post.slug}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
      <Counter />
    </>
  );
}

app/page.tsx/に対応します。このページはServer Componentのままですが、その中にClient ComponentのCounterを置けます。

動的ルートを作る

app/posts/[slug]/page.tsxを作ります。

import Link from "next/link";
import { notFound } from "next/navigation";
import { posts } from "../../post-data";

type Props = {
  params: Promise<{ slug: string }>;
};

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  const post = posts.find((item) => item.slug === slug);

  if (post === undefined) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <Link href="/">一覧へ戻る</Link>
    </article>
  );
}

[slug]はURLごとに変わる部分です。Next.js 16ではparamsはPromiseなので、async関数内でawait paramsしてからslugを読みます。該当記事がなければnotFound()を呼び、HTTP statusを404にします。

同じフォルダーへapp/posts/[slug]/not-found.tsxを作ります。

import Link from "next/link";

export default function NotFound() {
  return (
    <>
      <h1>記事が見つかりません</h1>
      <Link href="/">一覧へ戻る</Link>
    </>
  );
}

成功を確認する

npm run dev

http://localhost:3000を開き、次を確認します。

  1. 「最初の記事」を押すと/posts/first-postへ移動する
  2. 詳細ページにタイトルと本文が表示される
  3. 「一覧へ戻る」で/へ戻る
  4. 「読んだ回数」ボタンを押すと数値が増える
  5. /posts/not-foundを開くと「記事が見つかりません」と表示され、responseが404になる

404は別ターミナルからも確認できます。

curl -I http://localhost:3000/posts/not-found

先頭行がHTTP/1.1 404 Not FoundまたはHTTP/2 404なら成功です。

最後に本番用buildも確認します。

npm run build

よくあるエラー

params.slugを直接読んでエラーになる

Next.js 16のparamsはPromiseです。ページをasync関数にし、先にconst { slug } = await paramsとします。

useStateを使うとエラーになる

useStateを使うファイルの先頭に"use client"が必要です。layout.tsxpage.tsx全体へ付ける必要はありません。

ページが404になる

ファイル名がpage.tsxか、配置がapp/posts/[slug]/page.tsxか確認します。フォルダーを作るだけでは公開ページになりません。

練習

postsへ3件目の記事を追加し、一覧のリンクと詳細ページがコード変更なしで表示されることを確認してください。

次のステップ

このページ構成を公開する場合は、Vercelデプロイ入門へ進んでください。リクエスト処理を学ぶのは、画面のルーティングを理解してからで十分です。

参考リソース

(最終確認: 2026年7月25日)

← 一覧に戻る
PR
PR
PR
PR