Next.js: レイアウトとテンプレート

最終更新:2026-08-26

レイアウトシステムは建物のフロアのようなものです。各フロア (レイアウト) には共有の廊下と設備があり、各部屋 (ページ) は異なる装飾にでき、ユーザーの状態は階を移動しても保持されます。

1. 学ぶこと



2. フルスタック開発者の実話

(1) ペインポイント: 全ページにナビゲーションバーとサイドバーを繰り返し書かねばならない

Alice は TaskFlow 管理パネルの開発中に、レイアウトコードの重複問題に直面しました:

「私たちのチームには5人の開発者がいて、それぞれ異なるページを開発しています。みんな自分の page.tsx にナビゲーションバーとサイドバーを手動でインポートしなければなりません。先週、Charlie が新しく作成した settings/page.tsx にサイドバーを追加し忘れ、ユーザーが設定ページをクリックするとメニューが突然消え、ナビゲーションが壊れたと思われました。」

コードの重複:

問題 影響 影響を受けるページ数
ナビゲーションバーの重複インポート 全ページに手動で含める必要がある 15 ページ
サイドバーの状態が保持されない ナビゲーション後にサイドバーの選択状態が失われる 全ページ
ログイン/登録ページにナビゲーションが表示される 表示すべきでないもの、追加の条件チェックが必要 3 ページ
複雑なユーザーデータの伝達 全ページでユーザーデータを取得する必要がある 12 ページ

(2) Next.js レイアウトシステムの解決策

ネストレイアウトとルートグループを使用してレイアウトを分離し、一度定義すればグローバルに適用されます。

TEXT 📖 参照専用
src/app/
├── layout.tsx                  # ルートレイアウト (html/body/フォント)
├── page.tsx                    # ホーム
├── (auth)/
│   ├── layout.tsx              # ログイン/登録レイアウト (サイドバーなし)
│   ├── login/page.tsx
│   └── register/page.tsx
└── (dashboard)/
    ├── layout.tsx              # 管理パネルレイアウト (ナビ+サイドバー)
    ├── page.tsx                # ダッシュボード
    ├── projects/page.tsx       # プロジェクト一覧
    └── settings/page.tsx       # 設定ページ

(3) 効果

次元 導入前 (手動インポート) 導入後 (レイアウトシステム)
ナビゲーションバーの参照 15 ページで 15 行の import 1 つの layout.tsx ファイル
サイドバーの選択状態 消失 永続化
ログイン/登録レイアウト 条件チェック ルートグループによる自然な分離
ユーザーデータ取得 12 回のフェッチ 1 つの共有レイアウト


3. ネストレイアウトの原則

(1) レイアウトの永続化動作

100%
graph TB
    subgraph "ページナビゲーション"
        A[ルートレイアウト] --> B[ダッシュボードレイアウト]
        B --> C[ページ Dashboard]
        B --> D[ページ Project List]
        B --> E[ページ Settings]
    end

    subgraph "ナビゲーション中の動作"
        F[レイアウトはマウント維持<br/>状態をロスしない]
        G[ページはアンマウント/再マウント<br/>コンテンツ置換]
    end

    style F fill:#d4edda
    style G fill:#f8d7da
動作 レイアウト ページ
ナビゲーション中の再マウント ❌ 再マウントしない ✅ 再マウント
React 状態の保持 ✅ 保持される ❌ リセット
useEffect の再実行 ❌ 実行されない ✅ 実行される
データの再取得 ❌ 再取得しない ✅ 再取得

▶ サンプル: レイアウト永続化デモ

💻 出力:

TEXT 📖 参照専用
Diagram: layout persists across navigation (keeps state), template remounts (fresh instance).
TSX
// ============================================
// 永続化レイアウト vs ページ再マウントのデモ
// ============================================

// src/app/(dashboard)/layout.tsx — サイドバーレイアウト
'use client';

import { useState } from "react";
import Link from "next/link";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const [sidebarState, setSidebarState] = useState("collapsed");

  return (
    <div className="flex h-screen">
      {/* サイドバー — ナビゲーション中も展開/折りたたみ状態を保持 */}
      <aside className={`bg-gray-800 text-white ${sidebarState === "collapsed" ? "w-16" : "w-64"} transition-all`}>
        <button
          onClick={() => setSidebarState(s =>
            s === "collapsed" ? "expanded" : "collapsed"
          )}
          className="p-4 hover:bg-gray-700 w-full text-left"
        >
          {sidebarState === "collapsed" ? "→" : "← Collapse"}
        </button>
        <nav className="mt-4">
          <Link href="/dashboard" className="block p-3 hover:bg-gray-700">Dashboard</Link>
          <Link href="/dashboard/projects" className="block p-3 hover:bg-gray-700">Projects</Link>
          <Link href="/dashboard/settings" className="block p-3 hover:bg-gray-700">Settings</Link>
        </nav>
        <div className="mt-4 p-3 text-sm text-gray-400">
          Sidebar state persists across navigation
        </div>
      </aside>
      <main className="flex-1 p-8 overflow-auto">
        {children}
      </main>
    </div>
  );
}
💻 出力:

TEXT 📖 参照専用
Interactive component with state: sidebarState.
TSX
// src/app/(dashboard)/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <div>
      <h1 className="text-2xl font-bold">Dashboard</h1>
      <p className="text-gray-600">Welcome to your dashboard.</p>
    </div>
  );
}
TSX
// src/app/(dashboard)/dashboard/projects/page.tsx
export default function ProjectsPage() {
  return (
    <div>
      <h1 className="text-2xl font-bold">Projects</h1>
      <p className="text-gray-600">Your project list goes here.</p>
    </div>
  );
}
💻 出力:

TEXT 📖 参照専用
1. /dashboard にアクセス、サイドバーを展開、Dashboard コンテンツを表示
2. "→ Collapse" をクリック、サイドバーが 64px に折りたたまれる
3. "Projects" リンクをクリック、/dashboard/projects にナビゲート
4. サイドバーの折りたたみ状態が保持される (展開にリセットされない) ✅
5. ページコンテンツが Dashboard から Projects に変わる ✅

(2) ネストレイアウトの階層

100%
graph TB
    A[ルートレイアウト] --> B[app/layout.tsx]
    B --> C[(dashboard) レイアウト]
    C --> D[app/(dashboard)/layout.tsx]
    D --> E[products レイアウト]
    E --> F[app/(dashboard)/products/layout.tsx]
    F --> G[ページ]
    G --> H[app/(dashboard)/products/page.tsx]

    style B fill:#cce5ff
    style D fill:#d4edda
    style F fill:#f8d7da
レイアウトレベル スコープ 共有コンテンツ
ルートレイアウト 全ページ <html><body>、グローバルフォント、グローバルスタイル
グループレイアウト グループ内のページ サイドバー、ナビゲーションバー、ユーザー情報
ネストレイアウト サブディレクトリページ サブナビゲーション、パンくずリスト、ローカルフィルターバー

▶ サンプル: 3段ネストレイアウト

💻 出力:

TEXT 📖 参照専用
Diagram of nested layout hierarchy from root → parent → child layouts.
TSX
// ============================================
// 3段ネストレイアウト: グローバル → 管理画面 → 商品管理
// ============================================

// レベル1: src/app/layout.tsx — ルートレイアウト
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className="bg-gray-50">
        {children}
      </body>
    </html>
  );
}

// レベル2: src/app/(dashboard)/layout.tsx — 管理画面レイアウト
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  );
}

// レベル3: src/app/(dashboard)/products/layout.tsx — 商品管理レイアウト
export default function ProductsLayout({ children }) {
  return (
    <div>
      <nav className="flex gap-4 border-b pb-2 mb-4">
        <a href="/products" className="text-blue-600">All Products</a>
        <a href="/products/add" className="text-blue-600">Add Product</a>
        <a href="/products/categories" className="text-blue-600">Categories</a>
      </nav>
      {children}
    </div>
  );
}
💻 出力:

TEXT 📖 参照専用
RootLayout renders its UI.
💻 出力:

TEXT 📖 参照専用
/dashboard/products にアクセス:
→ ルートレイアウトが <html><body> をレンダリング
→ 管理画面レイアウトが <Sidebar> + <main> をレンダリング
→ 商品レイアウトが商品サブナビゲーション + ページコンテンツをレンダリング
3つのレベルすべてが有効


4. ルートグループ (group)

(1) ルートグループとは?

(group) ディレクトリは URL にパスセグメントを生成せず、論理グループ化のためだけに使用されます。

100%
graph LR
    subgraph "ファイル構造"
        A[app] --> B[(auth)]
        A --> C[(dashboard)]
        B --> D[login/page.tsx]
        B --> E[register/page.tsx]
        C --> F[page.tsx]
        C --> G[settings/page.tsx]
    end

    subgraph "対応する URL"
        H[/login]
        I[/register]
        J[/dashboard]
        K[/dashboard/settings]
    end

    style B fill:#f8d7da
    style C fill:#d4edda
ディレクトリ URL パス 説明
app/(auth)/login/page.tsx /login (auth) はパスセグメントを生成しない
app/(auth)/register/page.tsx /register (auth) はパスセグメントを生成しない
app/(dashboard)/page.tsx /dashboard (dashboard) はパスセグメントを生成しない
app/(dashboard)/settings/page.tsx /dashboard/settings サブパスは通常通り

▶ サンプル: ルートグループで異なるレイアウトを実装

💻 出力:

TEXT 📖 参照専用
Diagram of route structure: file system paths map to URL paths.
TSX
// ============================================
// ルートグループを使用してログインページと管理画面のレイアウトを分離
// ============================================

// src/app/(auth)/layout.tsx — ログイン/登録レイアウト (ナビなし、サイドバーなし)
export default function AuthLayout({ children }) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
      <div className="w-full max-w-md">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-bold text-gray-900">TaskFlow</h1>
          <p className="text-gray-500">Collaborate and deliver</p>
        </div>
        <div className="bg-white p-8 rounded-xl shadow-sm">
          {children}
        </div>
      </div>
    </div>
  );
}

// src/app/(auth)/login/page.tsx
export default function LoginPage() {
  return (
    <form className="space-y-4">
      <h2 className="text-xl font-bold text-center">Sign In</h2>
      <input
        type="email"
        placeholder="Email"
        className="w-full p-3 border rounded-lg"
      />
      <input
        type="password"
        placeholder="Password"
        className="w-full p-3 border rounded-lg"
      />
      <button
        type="submit"
        className="w-full p-3 bg-blue-600 text-white rounded-lg"
      >
        Sign In
      </button>
    </form>
  );
}
💻 出力:

TEXT 📖 参照専用
Renders: TaskFlow | Collaborate and deliver
TSX
// src/app/(dashboard)/layout.tsx — 管理パネルレイアウト (サイドバー + トップナビゲーション)
export default function DashboardLayout({ children }) {
  return (
    <div className="flex h-screen">
      <aside className="w-64 bg-gray-900 text-white">
        <div className="p-4 text-xl font-bold">TaskFlow</div>
        <nav className="mt-4">
          <a href="/dashboard" className="block p-3 hover:bg-gray-800">
            Dashboard
          </a>
          <a href="/dashboard/projects" className="block p-3 hover:bg-gray-800">
            Projects
          </a>
          <a href="/dashboard/settings" className="block p-3 hover:bg-gray-800">
            Settings
          </a>
        </nav>
      </aside>
      <div className="flex-1 flex flex-col">
        <header className="bg-white shadow-sm p-4">
          <input
            type="search"
            placeholder="Search..."
            className="w-64 p-2 border rounded"
          />
        </header>
        <main className="flex-1 p-8 overflow-auto">{children}</main>
      </div>
    </div>
  );
}
💻 出力:

TEXT 📖 参照専用
/login にアクセスすると表示:
- 中央配置のフォームデザイン (ナビゲーションバーなし、サイドバーなし)
- 青のグラデーション背景
- TaskFlow ブランドロゴ + サインインフォーム

/dashboard にアクセスすると表示:
- 左側のダークサイドバー (Dashboard / Projects / Settings)
- 上部の白い検索バー
- 右側のコンテンツエリア


5. レイアウト vs テンプレート

(1) 主な違い

100%
graph LR
    A[新しいページにナビゲート] --> B{レイアウト or テンプレート?}
    B -->|レイアウト| C[マウント維持<br/>状態永続化]
    B -->|テンプレート| D[アンマウントして再マウント<br/>再構築]
    C --> E[子コンポーネント更新]
    D --> F[子コンポーネント + すべてのラッパーコンテナが再構築]

    style C fill:#d4edda
    style D fill:#f8d7da
特性 layout.tsx template.tsx
ナビゲーション中の再マウント ❌ マウント維持 ✅ アンマウント + 再マウント
React 状態の保持 ✅ 保持される ❌ リセット
useEffect の再実行 ❌ 実行されない ✅ 実行される
ページ遷移アニメーション 不向き ✅ 適している
ナビゲーションごとのデータリフレッシュ ❌ リフレッシュしない ✅ 毎ナビゲーションでリフレッシュ
パフォーマンス より良い やや劣る (再構築のオーバーヘッド)

▶ サンプル: レイアウト vs テンプレートの動作比較

💻 出力:

TEXT 📖 参照専用
Diagram: layout persists across navigation (keeps state), template remounts (fresh instance).
TSX
// ============================================
// レイアウト vs テンプレートの動作比較デモ
// ============================================

// src/app/(dashboard)/layout.tsx — レイアウトを使用 (ナビゲーション中も状態を保持)
'use client';
import { useEffect, useState } from "react";
import Link from "next/link";

export default function DashboardLayout({ children }) {
  const [count, setCount] = useState(0);
  const [mountTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    console.log("Layout mounted at:", new Date().toLocaleTimeString());
  }, []);

  return (
    <div className="border-2 border-blue-500 p-4 rounded m-4">
      <div className="text-sm text-blue-600 mb-2">
        [LAYOUT] Mounted at: {mountTime} | Count: {count}
        <button onClick={() => setCount(c => c + 1)} className="ml-2 px-2 bg-blue-100 rounded">
          +1
        </button>
      </div>
      <nav className="flex gap-4 mb-4">
        <Link href="/dashboard/page-a" className="text-blue-600">Page A</Link>
        <Link href="/dashboard/page-b" className="text-blue-600">Page B</Link>
      </nav>
      {children}
    </div>
  );
}
💻 出力:

TEXT 📖 参照専用
Interactive component with state: count.
TSX
// ============================================
// template.tsx — ナビゲーション中に再構築
// ファイル: src/app/(dashboard)/template.tsx
// ============================================

'use client';
import { useEffect, useState } from "react";
import Link from "next/link";

export default function DashboardTemplate({ children }) {
  const [count, setCount] = useState(0);
  const [mountTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    console.log("Template mounted at:", new Date().toLocaleTimeString());
  }, []);

  return (
    <div className="border-2 border-red-500 p-4 rounded m-4">
      <div className="text-sm text-red-600 mb-2">
        [TEMPLATE] Mounted at: {mountTime} | Count: {count}
        <button onClick={() => setCount(c => c + 1)} className="ml-2 px-2 bg-red-100 rounded">
          +1
        </button>
      </div>
      <nav className="flex gap-4 mb-4">
        <Link href="/dashboard/page-a" className="text-red-600">Page A</Link>
        <Link href="/dashboard/page-b" className="text-red-600">Page B</Link>
      </nav>
      {children}
    </div>
  );
}
TSX
// src/app/(dashboard)/page-a.tsx
export default function PageA() {
  return <div className="text-lg">Page A Content</div>;
}

// src/app/(dashboard)/page-b.tsx
export default function PageB() {
  return <div className="text-lg">Page B Content</div>;
}
💻 出力:

TEXT 📖 参照専用
1. /dashboard/page-a にアクセス:
   [LAYOUT] Mounted at: 10:30:00 | Count: 0
   [TEMPLATE] Mounted at: 10:30:00 | Count: 0
   Page A Content

2. Count +1 ボタンをクリック (2つのコンテナの Count は独立して増加)

3. "Page B" リンクをクリック:
   [LAYOUT] Mounted at: 10:30:00 | Count: 1  ← レイアウトは維持、状態保持
   [TEMPLATE] Mounted at: 10:30:05 | Count: 0  ← テンプレートは再構築、状態リセット
   Page B Content

結論: レイアウトはマウントと状態を維持し、テンプレートは毎回のナビゲーションで再構築される

(2) テンプレートを使用するケース

シナリオ 推奨 理由
ページ遷移アニメーション テンプレート framer-motion の退出/進入アニメーション
毎ナビゲーションでデータをリフレッシュ テンプレート useEffect が再実行される
ナビゲーション分析トラッキング テンプレート 毎ナビゲーションでトラッキングコードをトリガー
ナビゲーションバー/サイドバー レイアウト 選択状態と展開/折りたたみを保持
ショッピングカート/プレイヤー レイアウト UI 状態の永続化

▶ サンプル: テンプレートを使ったページ遷移アニメーションの実装

💻 出力:

TEXT 📖 参照専用
The component renders the described UI in the browser.
TSX
// ============================================
// テンプレート + framer-motion でページ遷移アニメーションを実装
// ============================================

'use client';
import { motion } from "framer-motion";

export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}
💻 出力:

TEXT 📖 参照専用
Renders the Template component UI.
💻 出力:

TEXT 📖 参照専用
ページにナビゲートするたびに:
1. 古いページが上方向にフェードアウト (opacity: 1→0, y: 0→-20)
2. 新しいページが下からフェードイン (opacity: 0→1, y: 20→0)
3. アニメーション時間 300ms
4. レイアウトは維持され再構築されず、アニメーションはコンテンツエリアのみに適用


6. ルートレイアウトの必須設定

(1) ルートレイアウトの責務

ルートレイアウトは唯一の必須レイアウトファイルで、全ページのフレームワークを定義します:

設定項目 コード 説明
<html> タグ <html lang="en"> SEO に影響する言語属性
<body> タグ <body className="..."> グローバル CSS クラス
フォント読み込み next/font/google パフォーマンス最適化フォント
メタデータ export const metadata グローバル SEO メタデータ
グローバルスタイル import './globals.css' Tailwind ディレクティブ

▶ サンプル: 完全なルートレイアウト

💻 出力:

TEXT 📖 参照専用
The page renders as described above, with the UI updating based on the described behavior.
TSX
// ============================================
// ルートレイアウト — 完全な設定
// ファイル: src/app/layout.tsx
// ============================================

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

// Google フォント最適化 (自動プリロード + CSS size-adjust)
const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter",
});

// グローバル SEO メタデータ
export const metadata: Metadata = {
  title: {
    template: "%s | TaskFlow",
    default: "TaskFlow - Project Management",
  },
  description: "A collaborative project management platform",
  openGraph: {
    title: "TaskFlow",
    description: "Collaborate and deliver projects faster",
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className={inter.variable}>
      <body className="antialiased bg-gray-50 text-gray-900 min-h-screen">
        {children}
      </body>
    </html>
  );
}
💻 出力:

TEXT 📖 参照専用
Renders: Root layout with Inter font, global CSS classes, and SEO metadata template ("%s | TaskFlow").
💻 出力:

TEXT 📖 参照専用
全ページに自動生成:
- Inter フォント (パフォーマンス最適化、CLS ゼロ)
- グローバル CSS クラス (antialiased, bg-gray-50, text-gray-900)
- SEO メタデータ (タイトルテンプレート "ページ | TaskFlow")
- Open Graph タグ (SNS 共有カード)


7. データ共有モデルの設計

(1) 3つの共有モデル

100%
graph TB
    subgraph "データ共有モデル"
        A[1. Props 渡し<br/>レイアウト → ページ]
        B[2. Context Provider<br/>グローバル状態]
        C[3. 並列データ取得<br/>レイアウト + ページが個別に取得]
    end

    style A fill:#d4edda
    style B fill:#cce5ff
    style C fill:#f8d7da
モデル 適用シナリオ 利点 欠点
Props 渡し レイアウトがデータを取得してページに渡す 型安全 1段のみ渡せる
Context Provider ユーザー情報、テーマ グローバルに利用可能 クライアントコンポーネント必須
並列データ取得 レイアウトとページの独立したデータ 疎結合、並行処理 直接共有できない


8. 完全な例: TaskFlow 完全レイアウトシステム

TSX
// ============================================
// 総合例: TaskFlow 完全レイアウトシステム
// ルートレイアウト + 認証レイアウト + ダッシュボードレイアウト + 商品管理
// ============================================

// src/app/layout.tsx — ルートレイアウト
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"], display: "swap" });

export const metadata: Metadata = {
  title: { template: "%s | TaskFlow", default: "TaskFlow" },
  description: "Project management platform",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body className="bg-gray-50 antialiased">{children}</body>
    </html>
  );
}

// src/app/(auth)/layout.tsx — 認証レイアウト
export default function AuthLayout({ children }) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-600 to-indigo-700">
      <div className="w-full max-w-md">
        <div className="text-center text-white mb-8">
          <h1 className="text-4xl font-bold">TaskFlow</h1>
          <p className="text-blue-200 mt-2">Collaborate and deliver</p>
        </div>
        <div className="bg-white rounded-xl shadow-2xl p-8">
          {children}
        </div>
      </div>
    </div>
  );
}

// src/app/(dashboard)/layout.tsx — ダッシュボードレイアウト
'use client';
import { createContext, useContext, useState } from "react";
import Link from "next/link";

const DashboardContext = createContext(null);
export function useDashboard() { return useContext(DashboardContext); }

export default function DashboardLayout({ children }) {
  const [sidebarOpen, setSidebarOpen] = useState(true);
  const user = { name: "Alice", role: "Admin", avatar: "/avatar.png" };

  return (
    <DashboardContext.Provider value={{ user, sidebarOpen, setSidebarOpen }}>
      <div className="flex h-screen">
        <aside className={`bg-gray-900 text-white ${sidebarOpen ? "w-64" : "w-16"} transition-all duration-300`}>
          <div className="p-4 flex items-center gap-3">
            <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-sm">
              {user.name[0]}
            </div>
            {sidebarOpen && <span className="font-bold">TaskFlow</span>}
          </div>
          <nav className="mt-4">
            <Link href="/dashboard" className="flex items-center gap-3 p-3 hover:bg-gray-800">
              <span>📊</span>
              {sidebarOpen && <span>Dashboard</span>}
            </Link>
            <Link href="/dashboard/projects" className="flex items-center gap-3 p-3 hover:bg-gray-800">
              <span>📁</span>
              {sidebarOpen && <span>Projects</span>}
            </Link>
            <Link href="/dashboard/team" className="flex items-center gap-3 p-3 hover:bg-gray-800">
              <span>👥</span>
              {sidebarOpen && <span>Team</span>}
            </Link>
            <Link href="/dashboard/settings" className="flex items-center gap-3 p-3 hover:bg-gray-800">
              <span>⚙️</span>
              {sidebarOpen && <span>Settings</span>}
            </Link>
          </nav>
          <button
            onClick={() => setSidebarOpen(!sidebarOpen)}
            className="absolute bottom-4 left-4 text-gray-400 hover:text-white"
          >
            {sidebarOpen ? "◀" : "▶"}
          </button>
        </aside>
        <div className="flex-1 flex flex-col">
          <header className="bg-white shadow-sm px-8 py-4 flex items-center justify-between">
            <h2 className="text-lg font-semibold text-gray-700">
              Welcome, {user.name}
            </h2>
            <div className="flex items-center gap-4">
              <button className="text-gray-500">🔔</button>
              <div className="w-8 h-8 bg-gray-300 rounded-full" />
            </div>
          </header>
          <main className="flex-1 p-8 overflow-auto">{children}</main>
        </div>
      </div>
    </DashboardContext.Provider>
  );
}

// src/app/(dashboard)/dashboard/page.tsx — ダッシュボードホームページ
'use client';
import { useDashboard } from "../layout";

export default function DashboardHomePage() {
  const { user } = useDashboard();

  return (
    <div>
      <h1 className="text-3xl font-bold">Dashboard Overview</h1>
      <p className="text-gray-500 mt-2">
        Welcome back, {user.name}! Here is your project summary.
      </p>
      <div className="grid grid-cols-3 gap-6 mt-8">
        <div className="bg-white p-6 rounded-xl shadow-sm">
          <div className="text-sm text-gray-500">Active Projects</div>
          <div className="text-3xl font-bold mt-2">12</div>
        </div>
        <div className="bg-white p-6 rounded-xl shadow-sm">
          <div className="text-sm text-gray-500">Pending Tasks</div>
          <div className="text-3xl font-bold mt-2">48</div>
        </div>
        <div className="bg-white p-6 rounded-xl shadow-sm">
          <div className="text-sm text-gray-500">Team Members</div>
          <div className="text-3xl font-bold mt-2">8</div>
        </div>
      </div>
    </div>
  );
}

期待される出力:

TEXT 📖 参照専用
/login にアクセス:
- ダークブルーのグラデーション全画面背景
- 中央配置の白いフォームカード
- TaskFlow ロゴ + ログインフォーム

/dashboard にアクセス:
- 左側のダークサイドバー (折りたたみ可能、ナビゲーション中も状態維持)
- 上部ナビゲーションバー (Welcome, Alice 表示 + 通知アイコン)
- 右側のコンテンツエリア (Dashboard Overview + 3つの統計カード)
- ユーザーデータは Context 経由で共有

❓ よくある質問

Q layout.tsx に 'use client' を使えますか?
A はい。layout.tsx はデフォルトでサーバーコンポーネントですが、useState、useEffect、またはイベントハンドラを使用する必要がある場合、'use client' ディレクティブを追加できます。注意: 追加すると、このレイアウトとそのすべての子コンポーネントがクライアントコンポーネントになります。
Q ルートグループは URL にどのように影響しますか?
A まったく影響しません。(auth)/login/page.tsx の URL は /login のままです。(auth) はレイアウト構造にのみ影響し、URL パスセグメントを生成しません。これがルートグループのコア目的です。
Q レイアウトとテンプレートは共存できますか?
A はい。同じディレクトリに layout.tsx と template.tsx の両方が存在する場合、テンプレートはレイアウトの内部にラップされます。ユーザーがナビゲートすると、レイアウトは維持され、テンプレートは再構築されます。
Q ルートレイアウトの <html> タグは変更できますか?
A はい、手動で記述する必要があります。ルートレイアウトは Next.js のデフォルト HTML ラッパーを自動的に置き換えるため、langdirclassName などの属性を自由に追加できます。これは RTL サポートの鍵です。
Q ネストレイアウトの最大レベル数は?
A 厳密な制限はありませんが、3〜4レベルを超えないことを推奨します。レイアウトレベルごとに DOM にラッパー div が追加され、レベルが多すぎるとパフォーマンスとメンテナンス性に影響する可能性があります。
Q 異なるルートグループ間でデータを共有するには?
A ルートレイアウトの Context Provider を通じて共有します。ルートレイアウトで UserProviderThemeProvider などをラップすることで、すべてのルートグループ内のページがアクセスできます。これが Next.js が推奨するグローバルデータ共有方法です。

📖 まとめ


📝 練習問題

  1. 基礎問題 (⭐): プロジェクトに (marketing)/layout.tsx(app)/layout.tsx の2つのルートグループレイアウトを作成し、マーケティングページ (Home, About) とアプリページ (Dashboard, Settings) に異なるスタイルを適用してください。

  2. 発展問題 (⭐⭐): 同じディレクトリに layout.tsxtemplate.tsx の両方を配置し、各ファイルに useEffect(() => { console.log('mounted') }, []) を追加してページ間をナビゲートし、コンソール出力を観察してマウント方法の違いを検証してください。

  3. チャレンジ (⭐⭐⭐): ルートレイアウトに UserProvider (Context) を作成し、(auth)/login/page.tsx(dashboard)/page.tsx の両方で useUser() を使用してユーザーデータを読み取り、Context がルートグループ間でデータを共有することを検証してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%