今回やること
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を開き、次を確認します。
- 「最初の記事」を押すと
/posts/first-postへ移動する - 詳細ページにタイトルと本文が表示される
- 「一覧へ戻る」で
/へ戻る - 「読んだ回数」ボタンを押すと数値が増える
/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.tsxやpage.tsx全体へ付ける必要はありません。
ページが404になる
ファイル名がpage.tsxか、配置がapp/posts/[slug]/page.tsxか確認します。フォルダーを作るだけでは公開ページになりません。
練習
postsへ3件目の記事を追加し、一覧のリンクと詳細ページがコード変更なしで表示されることを確認してください。
次のステップ
このページ構成を公開する場合は、Vercelデプロイ入門へ進んでください。リクエスト処理を学ぶのは、画面のルーティングを理解してからで十分です。
参考リソース
- Next.js: Installation
- Next.js: Layouts and Pages
- Next.js: Dynamic Route Segments
- Next.js: Server and Client Components
- Next.js: Link
(最終確認: 2026年7月25日)
← 一覧に戻る