C
Next.js/Routing/Lesson 10

Layout vs Template — Persistent State vs Fresh Mount Every Time

25 min·theory
This chapter
2/3
TypeScript

Layout vs Template — Persistent State vs Fresh Mount Every Time

💡 Why Does This Matter? — Whether State Lives or Dies

🎯 In the App Router, when navigating between routes, `layout.tsx` is not remounted — meaning the `useState`, `useRef`, scroll position, and any playing video inside it are all preserved.
💼 This is desirable in some cases (sidebars, headers), but not in others (when you want to replay an entrance animation on every page visit).
`template.tsx` looks identical to `layout.tsx`, but **remounts on every navigation** — `useEffect` runs again each time.
🔗 In most cases (roughly 90%), `layout` is sufficient. Use `template` only for special cases such as entrance animations, per-page tracking, or forced state resets.
🏢 실무에서는
The sidebar on codemaster40 stays intact as you navigate between categories — this is the effect of `layout.tsx`. If the expand/collapse state of the sidebar were to reset on every category change, that would mean it was mistakenly implemented with `template.tsx`. Conversely, if you want a fresh fade-in animation on every page, `template.tsx` is the right choice.

Layout · Template · State Persistence Model

1. layout.tsx — Stays Mounted Across Child Navigations

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

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}
  • Navigating from /dashboard/posts to /dashboard/users: the layout stays intact; only children is swapped to the new page.
  • If a Client Component inside the layout holds useState, that state is preserved.

2. template.tsx — Freshly Mounted Every Time

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>
  );
}
  • Navigating from /dashboard/posts to /dashboard/users: the template remounts → the fade-in animation plays again.
  • Ideal for per-page entrance animations and pageview tracking (when useEffect must fire on every page entry).

3. Both Can Coexist

code
app/dashboard/
├── layout.tsx     ← sidebar (state preserved)
├── template.tsx   ← fade-in (remounts every time)
├── posts/page.tsx
└── users/page.tsx

Render order: layout > template > page. On navigation, the layout survives; the template and page remount each time.

4. Visualization — State Persistence Model

code
User: /dashboard/posts → /dashboard/users

[layout] -------- same instance kept (state survives)
  └─ [template] -------- new instance (state reset, useEffect re-runs)
       └─ [page] -------- new instance (as expected)

5. When to Use Which?

Use CaseRecommendation
Header · Sidebar · Footer (global shell)layout
Sidebar with expand/collapse statelayout (natural to preserve state)
Per-page fade-in / slide-in animationtemplate
useEffect must run on every page entry (analytics · pageview)template
Form auto-reset (clean slate after leaving and returning)template
90% of caseslayout

Use template only when you truly need it. Mindless use of template = remount cost on every navigation + visual flicker.

💻 🅰️ Anti-Pattern: Using Only template.tsx — State Resets Every Time
// ❌ Anti-pattern: Sidebar in template.tsx

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

export default function StudyTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  // ★ If user navigates from /study/javascript → /study/typescript,
  //   the template remounts → expanded resets to false
  const [expanded, setExpanded] = useState(false);

  return (
    <div>
      <aside>
        <button onClick={() => setExpanded(!expanded)}>
          {expanded ? 'Collapse' : 'Expand'}
        </button>
        {expanded && <NavList />}
      </aside>
      <main>{children}</main>
    </div>
  );
}

// User experience:
// 1. Enter /study → sidebar collapsed
// 2. User clicks "Expand" → opens
// 3. Enter /study/javascript → template remounts → collapses again 😡
// 4. Expand again → navigate to another category → resets again
//
// This state reset is a very annoying UX for users
💻 🅱️ The Correct Approach: Move to layout.tsx — State Preserved, Only the Page Swaps
// ✅ Layout for state persistence + template for entry animation if needed

// 📁 app/study/layout.tsx — Sidebar, state persistence
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 (holds state)
'use client';
import { useState } from 'react';

export function Sidebar() {
  // ★ This state persists across category navigation
  //   because the layout does not remount
  const [expanded, setExpanded] = useState(false);

  return (
    <aside>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Collapse' : 'Expand'}
      </button>
      {expanded && <NavList />}
    </aside>
  );
}

// 📁 app/study/template.tsx — Page-specific fade-in (optional)
'use client';
import { motion } from 'framer-motion';

export default function StudyTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  // ★ Fade-in again on every page navigation — because it's a template, it mounts every time
  return (
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.2 }}
    >
      {children}
    </motion.div>
  );
}

// User experience:
// 1. Enter /study → sidebar collapsed
// 2. User clicks "Expand" → opens
// 3. Enter /study/javascript → sidebar remains open 😊
//                              page content fades in (template)
// 4. Navigate to /study/typescript → sidebar remains, only content fades in
//
// State persists naturally, and content transitions are smooth

// Folder structure:
// app/study/
// ├── layout.tsx        ← Sidebar (state persistence)
// ├── template.tsx      ← Page entry animation (mounts every time)
// ├── Sidebar.tsx       ← Client
// ├── page.tsx          ← /study
// ├── [category]/page.tsx
// └── [category]/[slug]/page.tsx

💡 💡 Layout vs Template Decision Guide

1. Default to layout. Use template only when you can justify why.
Mindless use of template = remount cost every navigation + user frustration from unexpected state resets.

2. Ask yourself: 'Should this state survive page navigation?'

  • Yes → layout (sidebar · global shell · currently playing video player)
  • No → template (start clean on each page)

3. Three clear signals that you need template

  • Page transition animations (fade-in · slide-in)
  • Pageview analytics — useEffect must fire on every page entry
  • Form auto-reset — blank state even after leaving and returning

4. Even a 'use client' layout.tsx preserves its state
Whether the layout is a Server Component or a Client Component, the instance is kept alive. useState inside a Client layout works and persists normally.

5. Both only receive children — they cannot directly receive page props
Both layout and template can receive params (for dynamic routes), but they cannot receive data props from the page. Pages must fetch their own data.

⚡ Try It Yourself — Tracking layout vs template Instances

Simulate how layout and template behave differently across route navigations.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

If the sidebar's menu open/closed state must be preserved across category navigations, where should the sidebar be placed?
💡 `layout.tsx` is **not remounted** when navigating between routes under its folder. Any state held via `useState` in a Client Component inside it (e.g., expand/collapse) survives intact. If placed in `template.tsx` instead, the component remounts on every page navigation and its state resets to the initial value — resulting in a UX where 'the panel was expanded, but after navigating it collapses again'.
Layout vs Template — State Persistence vs Remounting - Next.js