モダンReact + Next.js #2 Next.jsの開始とApp Router

読了 9分

前回はなぜServer Componentsが必要なのか、その背景を扱いました。今回は実際に手を動かすコードに入ります。Next.jsプロジェクトを作成し、App Routerのファイルベースルーティングを身につけるのが目標です。

Next.jsプロジェクトの作成 #

新しい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ベースです。

注記
以前からあったPages Routerというシステムも依然としてNext.jsに残っていますが、新しいプロジェクトならApp Routerを使うのが標準です。2つのシステムはメンタルモデルが異なり、Server ComponentsはApp Routerでのみ正しく動作します。

作成が終わったらフォルダに入って開発サーバーを立ち上げます。

dev サーバー実行
cd modern-react-demo
npm run dev

http://localhost:3000 にアクセスするとNext.jsの初期画面が表示されます。

プロジェクト構造を見てみる #

初めて見る方には馴染みのある部分と見慣れない部分が混ざっているはずです。核心だけ整理すると:

modern-react-demo/
modern-react-demo/
├── public/                ← 静的ファイル (画像など)
├── src/
│   └── app/               ← ここが核心! ルーティングが始まるところ
│       ├── layout.js      ← すべてのページの共通レイアウト
│       ├── page.js        ← '/' パスのページ
│       ├── globals.css    ← グローバルスタイル
│       └── favicon.ico
├── package.json
├── next.config.mjs
└── jsconfig.json

Viteプロジェクトとの最大の違いは、src/app/フォルダのファイルとフォルダ構造そのものがルーティングになる点です。URLパスの一つ一つがフォルダで、その中のpage.jsが画面を描画します。これがファイルベースルーティングです。

最も単純なページ #

src/app/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:

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:

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で動的部分を取り出します。paramsPromiseなのでawaitする必要がある点に注意してください (Next.js 15から変更された部分)。

#15で扱ったReact RouterのuseParamsと似ていますが、ここではコンポーネントのpropsとして入ってきます。Server Componentの中ではフックが使えないからです (#3で詳しく扱います)。

リンクで移動する — <Link> #

ページ間の移動はNext.jsが提供するLinkコンポーネントで行います。通常の<a>はページのリロードを引き起こすので、クライアントサイド遷移のためには常にLinkを使ってください。

src/app/page.js
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 (すでに自動生成されている):

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がネストされるわけです。

ネスト 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:

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.jslayout.jsの他にも、フォルダに置くと自動的に動作する特別なファイルがあります。

ファイル役割
page.jsルートの画面 (必須)
layout.jsそのフォルダ以下の共通レイアウト
loading.jsSuspense fallback (#5で扱う)
error.jsエラー境界
not-found.js404画面
route.jsAPIルート (ページではないエンドポイント)
template.jslayoutに似ているが、毎回再マウントされるバージョン

今暗記する必要はなく、「こういうものがあるんだな」程度に覚えておけば十分です。シリーズを進めながら順に登場します。

動作確認 — 小さなサイトを作る #

これまで学んだことを総合して小さなサイトを作ってみましょう。

作る構造
src/app/
├── layout.js                     ← ヘッダー + フッター
├── page.js                       ← '/'
├── about/page.js                 ← '/about'
└── posts/
    ├── page.js                   ← '/posts' (一覧)
    └── [slug]/page.js            ← '/posts/[slug]' (詳細)

src/app/layout.js:

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:

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:

src/app/about/page.js
export default function AboutPage() {
  return (
    <div style={{ padding: '24px' }}>
      <h1>紹介</h1>
      <p>このサイトは学習用のデモです</p>
    </div>
  );
}

src/app/posts/page.js:

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:

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'ディレクティブの役割、そしてそれらをどう混ぜて使うべきかを学びましょう。

X