Next.js: レイアウトとテンプレート
最終更新:2026-08-26
レイアウトシステムは建物のフロアのようなものです。各フロア (レイアウト) には共有の廊下と設備があり、各部屋 (ページ) は異なる装飾にでき、ユーザーの状態は階を移動しても保持されます。
1. 学ぶこと
- ネストレイアウトの永続化動作と設定方法
- ルートグループ
(group)の論理グループ化と URL への影響 - レイアウトとテンプレートの主な違いと使い分け
- ルートレイアウトの必須設定:
<html>、<body>、フォント、メタデータ - レイアウト間のデータ共有の3つのモデル
2. フルスタック開発者の実話
(1) ペインポイント: 全ページにナビゲーションバーとサイドバーを繰り返し書かねばならない
Alice は TaskFlow 管理パネルの開発中に、レイアウトコードの重複問題に直面しました:
「私たちのチームには5人の開発者がいて、それぞれ異なるページを開発しています。みんな自分の page.tsx にナビゲーションバーとサイドバーを手動でインポートしなければなりません。先週、Charlie が新しく作成した
settings/page.tsxにサイドバーを追加し忘れ、ユーザーが設定ページをクリックするとメニューが突然消え、ナビゲーションが壊れたと思われました。」
コードの重複:
| 問題 | 影響 | 影響を受けるページ数 |
|---|---|---|
| ナビゲーションバーの重複インポート | 全ページに手動で含める必要がある | 15 ページ |
| サイドバーの状態が保持されない | ナビゲーション後にサイドバーの選択状態が失われる | 全ページ |
| ログイン/登録ページにナビゲーションが表示される | 表示すべきでないもの、追加の条件チェックが必要 | 3 ページ |
| 複雑なユーザーデータの伝達 | 全ページでユーザーデータを取得する必要がある | 12 ページ |
(2) Next.js レイアウトシステムの解決策
ネストレイアウトとルートグループを使用してレイアウトを分離し、一度定義すればグローバルに適用されます。
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) レイアウトの永続化動作
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 の再実行 | ❌ 実行されない | ✅ 実行される |
| データの再取得 | ❌ 再取得しない | ✅ 再取得 |
▶ サンプル: レイアウト永続化デモ
Diagram: layout persists across navigation (keeps state), template remounts (fresh instance).
// ============================================
// 永続化レイアウト 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>
);
}
Interactive component with state: sidebarState.
// 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>
);
}
// 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>
);
}
1. /dashboard にアクセス、サイドバーを展開、Dashboard コンテンツを表示
2. "→ Collapse" をクリック、サイドバーが 64px に折りたたまれる
3. "Projects" リンクをクリック、/dashboard/projects にナビゲート
4. サイドバーの折りたたみ状態が保持される (展開にリセットされない) ✅
5. ページコンテンツが Dashboard から Projects に変わる ✅
(2) ネストレイアウトの階層
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段ネストレイアウト
Diagram of nested layout hierarchy from root → parent → child layouts.
// ============================================
// 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>
);
}
RootLayout renders its UI.
/dashboard/products にアクセス:
→ ルートレイアウトが <html><body> をレンダリング
→ 管理画面レイアウトが <Sidebar> + <main> をレンダリング
→ 商品レイアウトが商品サブナビゲーション + ページコンテンツをレンダリング
3つのレベルすべてが有効
4. ルートグループ (group)
(1) ルートグループとは?
(group) ディレクトリは URL にパスセグメントを生成せず、論理グループ化のためだけに使用されます。
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 |
サブパスは通常通り |
▶ サンプル: ルートグループで異なるレイアウトを実装
Diagram of route structure: file system paths map to URL paths.
// ============================================
// ルートグループを使用してログインページと管理画面のレイアウトを分離
// ============================================
// 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>
);
}
Renders: TaskFlow | Collaborate and deliver
// 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>
);
}
/login にアクセスすると表示:
- 中央配置のフォームデザイン (ナビゲーションバーなし、サイドバーなし)
- 青のグラデーション背景
- TaskFlow ブランドロゴ + サインインフォーム
/dashboard にアクセスすると表示:
- 左側のダークサイドバー (Dashboard / Projects / Settings)
- 上部の白い検索バー
- 右側のコンテンツエリア
5. レイアウト vs テンプレート
(1) 主な違い
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 テンプレートの動作比較
Diagram: layout persists across navigation (keeps state), template remounts (fresh instance).
// ============================================
// レイアウト 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>
);
}
Interactive component with state: count.
// ============================================
// 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>
);
}
// 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>;
}
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 状態の永続化 |
▶ サンプル: テンプレートを使ったページ遷移アニメーションの実装
The component renders the described UI in the browser.
// ============================================
// テンプレート + 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>
);
}
Renders the Template component UI.
ページにナビゲートするたびに:
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 ディレクティブ |
▶ サンプル: 完全なルートレイアウト
The page renders as described above, with the UI updating based on the described behavior.
// ============================================
// ルートレイアウト — 完全な設定
// ファイル: 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>
);
}
Renders: Root layout with Inter font, global CSS classes, and SEO metadata template ("%s | TaskFlow").
全ページに自動生成:
- Inter フォント (パフォーマンス最適化、CLS ゼロ)
- グローバル CSS クラス (antialiased, bg-gray-50, text-gray-900)
- SEO メタデータ (タイトルテンプレート "ページ | TaskFlow")
- Open Graph タグ (SNS 共有カード)
7. データ共有モデルの設計
(1) 3つの共有モデル
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 完全レイアウトシステム
// ============================================
// 総合例: 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>
);
}
期待される出力:
/login にアクセス:
- ダークブルーのグラデーション全画面背景
- 中央配置の白いフォームカード
- TaskFlow ロゴ + ログインフォーム
/dashboard にアクセス:
- 左側のダークサイドバー (折りたたみ可能、ナビゲーション中も状態維持)
- 上部ナビゲーションバー (Welcome, Alice 表示 + 通知アイコン)
- 右側のコンテンツエリア (Dashboard Overview + 3つの統計カード)
- ユーザーデータは Context 経由で共有
❓ よくある質問
(auth)/login/page.tsx の URL は /login のままです。(auth) はレイアウト構造にのみ影響し、URL パスセグメントを生成しません。これがルートグループのコア目的です。<html> タグは変更できますか?lang、dir、className などの属性を自由に追加できます。これは RTL サポートの鍵です。div が追加され、レベルが多すぎるとパフォーマンスとメンテナンス性に影響する可能性があります。UserProvider、ThemeProvider などをラップすることで、すべてのルートグループ内のページがアクセスできます。これが Next.js が推奨するグローバルデータ共有方法です。📖 まとめ
- ネストレイアウトはナビゲーション中にマウントが維持され、React 状態が保持されます
- ルートグループ
(group)は URL パスセグメントを生成せず、論理グループ化とレイアウト分離のためだけに使用されます - レイアウトは永続的な UI に最適で、テンプレートはアニメーションとデータリフレッシュに最適です
- ルートレイアウトは全ページのフレームワークとして機能し、
<html>、<body>、メタデータを含める必要があります - Context Provider はレイアウト間でデータを共有する推奨方法です
- ルートグループの最も一般的なユースケース: ログインページ (ナビなし) と管理画面 (ナビあり) のレイアウト分離
- レイアウトはデフォルトでサーバーコンポーネントを使用します。インタラクションが必要な場合は 'use client' を追加します
📝 練習問題
-
基礎問題 (⭐): プロジェクトに
(marketing)/layout.tsxと(app)/layout.tsxの2つのルートグループレイアウトを作成し、マーケティングページ (Home, About) とアプリページ (Dashboard, Settings) に異なるスタイルを適用してください。 -
発展問題 (⭐⭐): 同じディレクトリに
layout.tsxとtemplate.tsxの両方を配置し、各ファイルにuseEffect(() => { console.log('mounted') }, [])を追加してページ間をナビゲートし、コンソール出力を観察してマウント方法の違いを検証してください。 -
チャレンジ (⭐⭐⭐): ルートレイアウトに UserProvider (Context) を作成し、
(auth)/login/page.tsxと(dashboard)/page.tsxの両方でuseUser()を使用してユーザーデータを読み取り、Context がルートグループ間でデータを共有することを検証してください。