C
Next.js/ルーティング/Lesson 10

Layout vs Template — 状態を維持するか、毎回新たにマウントするか

25分·theory
このチャプター
2/3
TypeScript

Layout vs Template — 状態を維持するか、毎回新たにマウントするか

💡 なぜ学ぶ必要があるのか? — 状態が生き残るか消えるか

🎯 App Router では、ルート間を移動しても `layout.tsx` は再マウントされません — つまり、内部の `useState`・`useRef`・スクロール位置・再生中の動画がそのまま保持されます。
💼 これが望ましい場合もあれば(サイドバー・ヘッダー)、そうでない場合もあります(ページ移動のたびに入場アニメーションを再生したいときなど)。
`template.tsx` は `layout.tsx` と見た目は同じですが、**移動のたびに再マウント**されます — `useEffect` が毎回実行されます。
🔗 通常は 90% のケースで `layout` で十分です。`template` は特殊なケース(入場アニメーション・ページ単位のトラッキング・強制リセット)にのみ使用します。
🏢 실무에서는
codemaster40 のサイドバーは、カテゴリ間を移動しても同じ状態を維持します — これが `layout.tsx` の効果です。カテゴリ移動のたびにサイドバーの開閉状態がリセットされるなら、それは `template.tsx` で誤って実装されたケースです。逆に、ページごとに新しいフェードインアニメーションを表示したい場合は `template.tsx` が適切です。

Layout · Template · 状態保持モデル

1. layout.tsx — 子ルート間のナビゲーションでもマウントを維持

tsx
// app/dashboard/layout.tsx
import { useState } from 'react';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}
  • /dashboard/posts/dashboard/users へ遷移したとき: layout はそのまま維持され、children だけが新しいページに差し替えられます。
  • layout 内の Client Component が useState を持っていれば、その state は保持されます。

2. template.tsx — 毎回新たにマウント

tsx
// app/dashboard/template.tsx
'use client';
import { motion } from 'framer-motion';

export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}
  • /dashboard/posts/dashboard/users へ遷移したとき: template が新たにマウントされ → フェードインアニメーションが再度実行されます。
  • ページごとの入場アニメーションや、useEffect をページ進入のたびに実行する必要があるページビュー計測に適しています。

3. 両方を同時に使うこともできる

code
app/dashboard/
├── layout.tsx     ← サイドバー (状態を保持)
├── template.tsx   ← フェードイン (毎回マウント)
├── posts/page.tsx
└── users/page.tsx

レンダー順: layout > template > page。ナビゲーション時、layout は生き残り、template と page は毎回新たに生成されます。

4. 可視化 — 状態保持モデル

code
ユーザー: /dashboard/posts → /dashboard/users

[layout] -------- 同じインスタンスを維持 (state が生き残る)
  └─ [template] -------- 新しいインスタンス (state リセット、useEffect 再実行)
       └─ [page] -------- 新しいインスタンス (当然)

5. いつどちらを使う?

ユースケース推奨
ヘッダー・サイドバー・フッター (グローバルシェル)layout
開閉状態を持つサイドバーlayout (state を保持するのが自然)
ページ単位のフェードイン / スライドインアニメーションtemplate
useEffect をページ進入のたびに実行する必要がある (分析・ページビュー)template
フォームの自動リセット (離脱後に戻っても空の状態)template
90% のケースlayout

template は本当に必要なときだけ使いましょう。安易な template の使用 = 毎回のマウントコスト + 画面のちらつきを招きます。

💻 🅰️ アンチパターン: template.tsx のみ使用 — 状態が毎回リセットされる
// ❌ template.tsx にサイドバーを置くアンチパターン

// 📁 app/study/template.tsx
'use client';
import { useState } from 'react';

export default function StudyTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  // ★ ユーザーが /study/javascript → /study/typescript に移動すると
  //   template が再マウント → expanded が再び false にリセット
  const [expanded, setExpanded] = useState(false);

  return (
    <div>
      <aside>
        <button onClick={() => setExpanded(!expanded)}>
          {expanded ? '折りたたむ' : '展開する'}
        </button>
        {expanded && <NavList />}
      </aside>
      <main>{children}</main>
    </div>
  );
}

// ユーザー体験:
// 1. /study にアクセス → サイドバーは折りたたまれた状態
// 2. ユーザーが「展開する」をクリック → 開く
// 3. /study/javascript にアクセス → template が再マウント → 再び折りたたまれる 😡
// 4. 再び展開 → 別のカテゴリに移動 → またリセット
//
// このような状態のリセットはユーザーにとって非常にイライラするUX
💻 🅱️ layout.tsx に移した正解 — 状態を保持し、ページだけが差し替わる
// ✅ layout で状態を維持 + 必要に応じて template でエントリーアニメーション

// 📁 app/study/layout.tsx — サイドバー、状態維持
import { Sidebar } from './Sidebar';

export default function StudyLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  );
}

// 📁 app/study/Sidebar.tsx — Client (状態を保持)
'use client';
import { useState } from 'react';

export function Sidebar() {
  // ★ このstateはカテゴリ移動しても生き残る
  //   layoutが再マウントされないため
  const [expanded, setExpanded] = useState(false);

  return (
    <aside>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? '折りたたむ' : '展開する'}
      </button>
      {expanded && <NavList />}
    </aside>
  );
}

// 📁 app/study/template.tsx — ページごとのフェードイン (オプション)
'use client';
import { motion } from 'framer-motion';

export default function StudyTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  // ★ ページ移動ごとにフェードインを再実行 — templateなので毎回マウント
  return (
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.2 }}
    >
      {children}
    </motion.div>
  );
}

// ユーザー体験:
// 1. /study にアクセス → サイドバーは折りたたまれた状態
// 2. ユーザーが「展開する」をクリック → 開く
// 3. /study/javascript にアクセス → サイドバーは開いたまま維持 😊
//                              ページ本文はフェードイン (template)
// 4. /study/typescript に移動 → サイドバーはそのまま、本文のみフェードイン
//
// 状態が自然に維持され、本文の切り替えはスムーズ

// フォルダー構造:
// app/study/
// ├── layout.tsx        ← サイドバー (状態維持)
// ├── template.tsx      ← ページエントリーアニメーション (毎回マウント)
// ├── Sidebar.tsx       ← Client
// ├── page.tsx          ← /study
// ├── [category]/page.tsx
// └── [category]/[slug]/page.tsx

💡 💡 Layout vs Template 判断ガイド

1. 基本は常に layout。template は「なぜ必要か」答えられるときだけ使う
安易な template の使用 = 毎回のマウントコスト + 意図しない state リセットによるユーザーの不満。

2. 「state がページ遷移をまたいで生き残るべきか?」を考える

  • はい → layout (サイドバー・グローバルシェル・再生中のビデオプレイヤー)
  • いいえ → template (ページごとにクリーンな状態で開始)

3. template が必要な明確なシグナル 3 つ

  • ページ遷移アニメーション (フェードイン・スライドイン)
  • ページビュー分析 — useEffect をページ進入のたびに実行する必要がある
  • フォームの自動リセット — 離脱後に戻っても空の状態

4. layout.tsx に 'use client' を付けても state は保持される
layout が Server Component であれ Client Component であれ、インスタンスが維持されるという事実は変わりません。Client layout の useState は正常に保持されます。

5. どちらも children のみを受け取る — ページの props を直接受け取ることはできない
layout・template ともに params は受け取れます (動的ルート) が、ページのデータ props を受け取ることはできません。データはページが自分で fetch します。

⚡ 自分で試してみよう — layout vs template のインスタンス追跡

ルート遷移のたびに layout と template がどのように異なる動作をするかをシミュレーションします。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

理解度チェッククイズ

サイドバーの「メニューの開閉」状態がカテゴリー遷移をまたいでもそのまま保持されるべき場合、そのサイドバーはどこに置くべきでしょうか?
💡 `layout.tsx` は、そのフォルダ配下のルート間を移動しても**再マウントされません**。内部の Client Component が `useState` で保持する状態(例:開閉)はそのまま生き続けます。`template.tsx` に配置した場合は、ページ移動のたびにコンポーネントが再マウントされ、状態が初期値にリセットされます — 「開いていたのに移動したらまた閉じている」という UX になります。
Layout vs Template — 状態維持 vs 毎回マウント - Next.js