We spent about three hours debugging a wizard flow last year because we used a layout where we needed a template. The state wasn't resetting between steps, the form was carrying over values it shouldn't have, and we kept staring at the code like it was lying to us. It wasn't. We were just using the wrong tool.
If you're building with the Next.js App Router, you've probably used `layout.tsx` already. Maybe you've even seen `template.tsx` in the docs, thought "huh, interesting" and moved on. This post is the explanation we wish existed before we wasted that afternoon.
The Core Difference (It's About Mounting)
Here's the one-sentence version: layouts persist across route changes, templates remount. That's it. Everything else flows from this.
When a user navigates between routes that share a layout, Next.js keeps the layout component mounted. The layout's state survives. Effects don't re-run. Your sidebar doesn't flash. Your navigation doesn't re-fetch. This is almost always what you want.
Templates are the opposite. Every time the user navigates to a route that uses a template, Next.js unmounts the old template instance and mounts a fresh one. State resets to initial values. Effects re-run. The component starts from scratch.
Layout = the same instance persists. Template = a new instance on every navigation.
How Layouts Actually Work
You've probably written this a hundred times already:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto p-6">
{children}
</main>
</div>
)
}When the user moves from `/dashboard/overview` to `/dashboard/settings`, that `DashboardLayout` component does not unmount. The `Sidebar` component does not unmount. Any state inside `DashboardLayout` — say, a collapsed/expanded sidebar toggle — stays exactly as it was. This is the behavior you want 90% of the time.
Layouts can also be nested. You have your root layout at `app/layout.tsx`, then `app/dashboard/layout.tsx`, then maybe `app/dashboard/settings/layout.tsx`. They all wrap around each other like matryoshka dolls, and they all persist independently across their respective route segments.
One thing that trips people up: layouts don't receive `searchParams`. If you need to respond to query string changes in a wrapper component, you're either reaching for a template, or you need to move that logic into the page itself. We've seen codebases where someone spent days wondering why their layout wasn't updating on filter changes — it was never going to, because the layout never re-renders on `?sort=asc` changes.
How Templates Work
The API looks almost identical, which is why the behavior difference catches people off guard:
// app/onboarding/template.tsx
'use client'
import { useEffect } from 'react'
import { trackPageView } from '@/lib/analytics'
export default function OnboardingTemplate({
children,
}: {
children: React.ReactNode
}) {
// This runs on EVERY navigation within /onboarding/*
useEffect(() => {
trackPageView()
}, [])
return (
<div className="onboarding-wrapper">
{children}
</div>
)
}Every time the user hits a new route under `/onboarding/`, this component mounts fresh. That `useEffect` fires. Any state inside resets. This is exactly what you want for things like analytics tracking, animation entry states, or form flows where you explicitly don't want state bleeding between steps.
When to Use a Template
There are specific scenarios where templates are the right call. If you reach for a layout in any of these, you'll be in debugging hell faster than you think.
- Page transition animations — if you're using Framer Motion or CSS animations on entry, you need the component to mount fresh so the animation actually triggers on every page visit
- Multi-step wizards or onboarding flows — each step should start clean, with no state leaking from the previous step
- Per-page analytics tracking — `useEffect` in a layout won't fire on subsequent navigations since the component stays mounted
- Forms that should reset between routes — a contact form at `/contact/sales` and `/contact/support` should not share state
- Any scenario where 'enter this route' needs to mean 'start fresh'
Here's the animation example we use most often. Without a template, your entry animation fires once and never again:
// app/(marketing)/template.tsx
'use client'
import { motion } from 'framer-motion'
export default function MarketingTemplate({
children,
}: {
children: React.ReactNode
}) {
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
{children}
</motion.div>
)
}Put this in a layout and it animates exactly once — when the user first loads a marketing page. Navigate to another marketing page and nothing happens, because the layout is already mounted. Template makes every navigation feel alive.
When to Use a Layout
This is the default. If you're not sure, use a layout. The cases where you need a template are specific; everything else should be a layout.
- Navigation and sidebars — you don't want these re-mounting on every page change
- Persistent UI like a music player, chat widget, or notification center
- Authentication wrappers — no reason to re-run auth checks on every navigation
- Shared data fetching — layouts can be async server components, fetch once, serve to all child routes
- Any UI that should feel continuous across navigations
// app/(app)/layout.tsx
import { getUser } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function AppLayout({
children,
}: {
children: React.ReactNode
}) {
const user = await getUser()
if (!user) {
redirect('/login')
}
return (
<div className="min-h-screen bg-background">
<TopNav user={user} />
<div className="flex">
<AppSidebar />
<main className="flex-1">{children}</main>
</div>
</div>
)
}This is the classic auth-protected app shell. The `getUser()` call runs once when the user enters this route segment, not on every page transition. The nav and sidebar stay mounted. This is performant and correct behavior.
You Can Use Both at the Same Level
This is the part most people miss: you can have both a `layout.tsx` and a `template.tsx` in the same directory. They compose. The layout wraps the template, and the template wraps the page.
// The render order is:
// layout.tsx → template.tsx → page.tsx
// app/dashboard/layout.tsx — persists, keeps sidebar mounted
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
)
}
// app/dashboard/template.tsx — remounts on every dashboard page nav
export default function DashboardTemplate({ children }) {
// track every dashboard page view, animate each page entry
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{children}
</motion.div>
)
}With this setup: sidebar persists (good, no flash), but the content area fades in fresh on every navigation (good, feels responsive). You're not choosing one or the other — you're using each for exactly what it's good at.
The Mistakes We've Actually Made
Beyond the wizard fiasco mentioned at the top, we've hit a few other layouts-vs-templates gotchas worth calling out.
**Analytics in layouts.** We had a `useEffect` in a layout calling our analytics tracking function. It fired on first load and never again for that route segment. We thought our analytics were broken for weeks. They weren't — we were just in a layout. Moved it to a template, problem solved immediately.
**Scroll position.** If you have a scrollable container in a layout, its scroll position persists between navigations. Sometimes that's what you want (infinite scroll feed). Often it's not (user clicks to a new page and is halfway down). Templates reset scroll naturally because the element unmounts and remounts. With layouts, you need to handle scroll restoration manually.
**Focus management.** Accessibility-wise, templates play nicer for flows where keyboard focus should reset on page change. With a persistent layout, focus can end up in unexpected places after navigation. If you're building something that needs to be properly accessible, think through where templates can help you here.
If your useEffect should fire on every page navigation within a segment, use a template. If it should fire once when the user enters the segment, use a layout.
Most of the templates we ship at peal.dev use both — a root layout for the app shell (auth, nav, sidebar), and targeted templates for flows where fresh-mount behavior matters, like onboarding sequences or content areas with page transitions. Getting this right from the start means you're not retrofitting the architecture later.
The mental model that makes this click: think about whether you're describing a container that should exist continuously while the user is in a section of your app, or a wrapper that should greet the user fresh every time they hit a new page. Continuous = layout. Fresh greeting = template. Once you frame it that way, the right choice is usually obvious.
