モダンReact + Next.js #2 Next.jsの開始とApp Router
前回はなぜServer Componentsが必要なのか、その背景を扱いました。今回は実際に手を動かすコードに入ります。Next.jsプロジェクトを作成し、App Routerのファイルベースルーティングを身につけるのが目標です。
Next.jsプロジェクトの作成 #
npx create-next-app@latest modern-react-demo質問が出たら次のように選択します (このシリーズ基準)。
✔ TypeScript? ........ No
✔ ESLint? ............ Yes
✔ Tailwind CSS? ...... No
✔ src/ directory? .... Yes
✔ App Router? ........ Yes (必ず Yes!)
✔ Turbopack? ......... Yes
✔ import alias? ...... No最も重要なのはApp RouterをYesで選択することです。App RouterがServer Componentsをサポートする新しいルーターであり、このシリーズはすべてApp Routerベースです。
作成が終わったらフォルダに入って開発サーバーを立ち上げます。
cd modern-react-demo
npm run devhttp://localhost:3000 にアクセスするとNext.jsの初期画面が表示されます。
プロジェクト構造を見てみる #
初めて見る方には馴染みのある部分と見慣れない部分が混ざっているはずです。核心だけ整理すると:
modern-react-demo/
├── public/ ← 静的ファイル (画像など)
├── src/
│ └── app/ ← ここが核心! ルーティングが始まるところ
│ ├── layout.js ← すべてのページの共通レイアウト
│ ├── page.js ← '/' パスのページ
│ ├── globals.css ← グローバルスタイル
│ └── favicon.ico
├── package.json
├── next.config.mjs
└── jsconfig.jsonViteプロジェクトとの最大の違いは、src/app/フォルダのファイルとフォルダ構造そのものがルーティングになる点です。URLパスの一つ一つがフォルダで、その中のpage.jsが画面を描画します。これがファイルベースルーティングです。
最も単純なページ #
src/app/page.jsを空にして新しく書き直してみましょう。
export default function HomePage() {
return (
<main style={{ padding: '24px' }}>
<h1>ホームページ</h1>
<p>モダンリアクトシリーズへようこそ。</p>
</main>
);
}保存すると/パスが更新されます。このコンポーネントはServer Componentです — 'use client'がなければデフォルトでそうなるからです。コンソールやdevサーバーターミナルにconsole.logを仕込んでみると、その出力はブラウザではなくdevサーバー側に表示されます。
export default function HomePage() {
console.log('これがどこに出力されるか?'); // dev サーバーターミナル!
return <h1>ホームページ</h1>;
}サーバーで実行されるコードであることがこのように直感的に確認できます。詳細は#3で扱います。
新しいルートの追加 #
/aboutページを作ってみましょう。フォルダを作ってその中にpage.jsを置けば終わりです。
src/app/
├── layout.js
├── page.js ← '/'
└── about/
└── page.js ← '/about'src/app/about/page.js:
export default function AboutPage() {
return (
<main style={{ padding: '24px' }}>
<h1>紹介</h1>
<p>このサイトは Next.js 学習用のデモです。</p>
</main>
);
}http://localhost:3000/about にアクセスすると新しいページが表示されます。ルーティング設定のコードを1行も書いていないのに、フォルダだけでルーティングが行われたわけです。
動的パス #
URLに動的パラメータが入るルートは、フォルダ名を[parameter]形式にして作ります。
src/app/
└── posts/
└── [slug]/
└── page.js ← '/posts/anything'src/app/posts/[slug]/page.js:
export default async function PostPage({ params }) {
const { slug } = await params;
return (
<main style={{ padding: '24px' }}>
<h1>ポスト: {slug}</h1>
<p>このページのスラグは "{slug}" です。</p>
</main>
);
}/posts/hello-world、/posts/react-nyumonのようなURLがすべてこのファイルにマッチし、params.slugで動的部分を取り出します。paramsはPromiseなのでawaitする必要がある点に注意してください (Next.js 15から変更された部分)。
#15で扱ったReact RouterのuseParamsと似ていますが、ここではコンポーネントのpropsとして入ってきます。Server Componentの中ではフックが使えないからです (#3で詳しく扱います)。
リンクで移動する — <Link>
#
ページ間の移動はNext.jsが提供するLinkコンポーネントで行います。通常の<a>はページのリロードを引き起こすので、クライアントサイド遷移のためには常にLinkを使ってください。
import Link from 'next/link';
export default function HomePage() {
return (
<main style={{ padding: '24px' }}>
<h1>ホームページ</h1>
<ul>
<li><Link href="/about">紹介</Link></li>
<li><Link href="/posts/hello-world">最初のポスト</Link></li>
</ul>
</main>
);
}Linkは画面に表示され始めると、そのページを事前にprefetchまでしてくれて、クリック直後に遷移できるようにしてくれます。
Layout — 共通の枠 #
Webサイトのヘッダー、フッター、サイドバーのように、複数のページで共有される部分をどう処理するでしょうか? Next.jsではlayout.jsファイルがその役割を果たします。
src/app/layout.js (すでに自動生成されている):
import './globals.css';
export const metadata = {
title: 'モダンリアクトデモ',
description: 'Next.js 学習用',
};
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<header style={{ padding: '12px', background: '#f4f4f4' }}>
<strong>私のサイト</strong>
</header>
<div>{children}</div>
<footer style={{ padding: '12px', background: '#f4f4f4', marginTop: '40px' }}>
© 2026
</footer>
</body>
</html>
);
}ポイント:
<html>と<body>がここにある必要がある (root layoutがページの骨格)childrenはそのlayout以下のページ (またはさらに下位のlayout)metadataは<head>情報 — Next.jsが自動で処理
これですべてのページにヘッダーとフッターが自動的に付きます。各page.jsは本文部分だけ書けば良いことになります。
ネストlayout #
フォルダにlayout.jsを置くと、そのフォルダと下位パスにのみ適用されるlayoutを追加できます。layoutがネストされるわけです。
src/app/
├── layout.js ← すべてのページ共通 (root layout)
├── page.js ← '/'
└── docs/
├── layout.js ← '/docs/...' すべてのページに適用
├── page.js ← '/docs'
└── [slug]/
└── page.js ← '/docs/anything'src/app/docs/layout.js:
import Link from 'next/link';
export default function DocsLayout({ children }) {
return (
<div style={{ display: 'flex', gap: '24px', padding: '24px' }}>
<aside style={{ width: '180px', borderRight: '1px solid #eee', paddingRight: '16px' }}>
<h3>ドキュメント</h3>
<ul>
<li><Link href="/docs/intro">はじめに</Link></li>
<li><Link href="/docs/api">API</Link></li>
</ul>
</aside>
<section style={{ flex: 1 }}>
{children}
</section>
</div>
);
}これで/docsで始まるすべてのページにサイドバーが自動的に付きます。他のパス(/about、/posts/...)には影響ありません。layoutがページツリーに従って自然にネストされるわけです。
ページ間を移動するとき、layout自体は再マウントされず、変更された部分だけが再描画されます。だからサイドバーのスクロール位置のようなものが維持される滑らかなUXが自然と生まれます。
特別なファイル — 一覧 #
App Routerにはpage.js、layout.jsの他にも、フォルダに置くと自動的に動作する特別なファイルがあります。
| ファイル | 役割 |
|---|---|
page.js | ルートの画面 (必須) |
layout.js | そのフォルダ以下の共通レイアウト |
loading.js | Suspense fallback (#5で扱う) |
error.js | エラー境界 |
not-found.js | 404画面 |
route.js | APIルート (ページではないエンドポイント) |
template.js | layoutに似ているが、毎回再マウントされるバージョン |
今暗記する必要はなく、「こういうものがあるんだな」程度に覚えておけば十分です。シリーズを進めながら順に登場します。
動作確認 — 小さなサイトを作る #
これまで学んだことを総合して小さなサイトを作ってみましょう。
src/app/
├── layout.js ← ヘッダー + フッター
├── page.js ← '/'
├── about/page.js ← '/about'
└── posts/
├── page.js ← '/posts' (一覧)
└── [slug]/page.js ← '/posts/[slug]' (詳細)src/app/layout.js:
import Link from 'next/link';
import './globals.css';
export const metadata = {
title: 'モダンリアクトデモ',
};
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<header style={{ padding: '12px 24px', background: '#222', color: '#fff' }}>
<Link href="/" style={{ color: '#fff', textDecoration: 'none', marginRight: '16px' }}>ホーム</Link>
<Link href="/about" style={{ color: '#fff', textDecoration: 'none', marginRight: '16px' }}>紹介</Link>
<Link href="/posts" style={{ color: '#fff', textDecoration: 'none' }}>ポスト</Link>
</header>
<main>{children}</main>
</body>
</html>
);
}src/app/page.js:
export default function HomePage() {
return (
<div style={{ padding: '24px' }}>
<h1>ホーム</h1>
<p>Next.js で作ったモダンリアクトデモです。</p>
</div>
);
}src/app/about/page.js:
export default function AboutPage() {
return (
<div style={{ padding: '24px' }}>
<h1>紹介</h1>
<p>このサイトは学習用のデモです。</p>
</div>
);
}src/app/posts/page.js:
import Link from 'next/link';
const POSTS = [
{ slug: 'hello-world', title: '最初の記事' },
{ slug: 'about-rsc', title: 'RSCとは?' },
{ slug: 'tips', title: '学習のヒント' },
];
export default function PostsPage() {
return (
<div style={{ padding: '24px' }}>
<h1>ポスト</h1>
<ul>
{POSTS.map(post => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}src/app/posts/[slug]/page.js:
export default async function PostPage({ params }) {
const { slug } = await params;
return (
<div style={{ padding: '24px' }}>
<h1>{slug}</h1>
<p>このページはスラグ "{slug}" の本文です。</p>
</div>
);
}保存してヘッダーのリンクをクリックして移動してみてください。画面遷移がチラつきなく滑らかに行われ、URLも正しく更新されます。
まとめ #
今回の記事ではNext.jsの開始とApp Routerの核心を扱いました。
npx create-next-appでプロジェクト作成 (App Router選択必須)src/app/のフォルダ構造がそのままルーティングpage.js= 画面、layout.js= 共有の枠- 動的パスは
[param]フォルダ名 <Link>でクライアントサイド遷移- layoutは自然にネストされる
これまで作ったページはすべてServer Componentsでした。しかしクリックイベントやuseStateのようなインタラクションが入るとどうなるでしょうか? 次の記事「モダンReact + Next.js #3 Server Components vs Client Components」では、2種類のコンポーネントの違いを明確にし、'use client'ディレクティブの役割、そしてそれらをどう混ぜて使うべきかを学びましょう。