Layouts & Templates
The Persistent Shell vs the Fresh Start
LinkedIn Hook
Every Next.js app has a layout. Most developers never think about it beyond wrapping their pages in a nav bar.
But in interviews, "Tell me about layouts and templates in Next.js" is where the gap between surface-level knowledge and real understanding becomes obvious.
Here's the key insight:
layout.jspersists across navigation — it never re-mounts.template.jsre-mounts on every navigation — it gets a fresh instance each time. Understanding why these two exist and when to reach for each one is what separates developers who use Next.js from developers who truly understand it.In Lesson 5.2, I break down layouts, templates, the root layout requirement, nested layouts, and the state-sharing patterns that interviewers love to ask about.
Read the full lesson -> [link]
#NextJS #React #WebDevelopment #FrontendDevelopment #InterviewPrep #AppRouter #Routing
What You'll Learn
- What
layout.jsdoes and why it persists across navigations without re-mounting - What the root layout is and why Next.js requires it
- How nested layouts compose to create complex UI shells
- What
template.jsdoes differently and when you need a fresh mount on every navigation - How to share state through layouts without prop drilling
- The mental model for choosing between layout and template in any scenario
The Building Analogy — Permanent Walls vs Temporary Partitions
Think of a large office building. The layout is the building itself — the outer walls, the elevator shafts, the reception desk, the hallway structure. When employees move between floors (navigate between pages), the building doesn't demolish itself and rebuild. The walls stay. The elevator keeps running. The reception desk remains exactly where it was.
Now think of templates as temporary exhibition partitions inside a conference hall. Every time a new exhibition starts (a new navigation happens), the old partitions are torn down and brand-new ones are erected. The hall is the same, but everything inside it is fresh. Counters reset to zero, animations replay from the beginning, and any state from the previous exhibition is gone.
This is the core distinction in Next.js:
layout.js= the building. Persistent. Shared across navigations. Never re-mounts.template.js= the exhibition partition. Fresh every time. Re-mounts on every navigation.
+------------------------------------------------------------------+
| THE BUILDING MODEL |
+------------------------------------------------------------------+
| |
| layout.js [ Building walls ] --> Always standing |
| Elevator, lobby State preserved |
| Navigation bar No re-mount |
| |
| template.js [ Exhibition walls ] --> Torn down & rebuilt |
| Temporary displays State reset |
| Fresh counters Re-mounts every navigation |
| |
+------------------------------------------------------------------+
Napkin AI Visual Prompt: "Dark gradient (#0a0e1a -> #111827). Left: a solid green (#10b981) building outline with floors labeled '/about', '/blog', '/contact' — the outer walls never change, an arrow labeled 'persists' wraps around. Right: a purple (#8b5cf6) dashed-outline room with a circular 're-mount' arrow, showing the walls dissolving and reforming. Title: 'Layout vs Template'. White monospace labels."
Understanding layout.js — The Persistent Shell
A layout in Next.js is a UI component that wraps one or more pages (or nested layouts) and persists across navigations. When a user navigates from /dashboard/settings to /dashboard/analytics, the dashboard layout does not unmount and remount. React preserves its component instance, which means:
- State inside the layout is kept alive
- Effects (
useEffect) do not re-run - DOM elements are not destroyed and recreated
- Expensive computations are not repeated
- Scroll position in the layout's sidebar stays put
Here is what a basic layout looks like:
// app/dashboard/layout.tsx
// This layout wraps ALL pages under /dashboard/*
// It persists when navigating between /dashboard/settings and /dashboard/analytics
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// This component mounts ONCE and stays alive across all /dashboard/* navigations
return (
<div className="flex min-h-screen">
{/* Sidebar never re-mounts when switching pages */}
<aside className="w-64 bg-gray-900 p-4">
<nav>
<a href="/dashboard/settings">Settings</a>
<a href="/dashboard/analytics">Analytics</a>
<a href="/dashboard/billing">Billing</a>
</nav>
</aside>
{/* Only this part changes — the child page swaps in and out */}
<main className="flex-1 p-8">
{children}
</main>
</div>
);
}
The {children} prop is the key. When the user navigates from one page to another within the same layout boundary, Next.js only swaps the content that fills {children}. The layout itself — the sidebar, the header, the navigation — stays exactly as it was.
This behavior is not optional or configurable. It is the fundamental design of layout.js in the App Router. Layouts always persist.
Why Persistence Matters
Consider a dashboard sidebar that fetches a list of navigation items from an API, or a layout that maintains a WebSocket connection for real-time notifications. If the layout re-mounted on every navigation:
- The API would be called again unnecessarily
- The WebSocket would disconnect and reconnect
- Any unsaved form data in the sidebar would be lost
- Loading spinners would flash on every page change
Layout persistence eliminates all of these problems. The layout mounts once, and the page content inside it is the only thing that changes.
The Root Layout — The One Layout You Must Have
Every Next.js App Router application requires a root layout at app/layout.tsx. This is non-negotiable — if you delete it, Next.js will throw an error. The root layout is the topmost wrapper for your entire application, and it has two requirements that no other layout has:
- It must include the
<html>tag - It must include the
<body>tag
// app/layout.tsx — THE ROOT LAYOUT (required)
// This is the only layout that must contain <html> and <body> tags
// Every single page in your application is wrapped by this layout
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
// Metadata for the entire application (can be overridden by child pages)
export const metadata: Metadata = {
title: {
default: 'My Application',
template: '%s | My Application',
},
description: 'A Next.js application with proper layout architecture.',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
// The <html> and <body> tags MUST be here — nowhere else
<html lang="en">
<body className={inter.className}>
{/* Global providers go here — they wrap the entire app */}
<ThemeProvider>
<AuthProvider>
{/* Global UI elements */}
<Header />
{/* All page content flows through here */}
{children}
{/* Global footer */}
<Footer />
</AuthProvider>
</ThemeProvider>
</body>
</html>
);
}
Why does Next.js require this? Because the App Router uses React Server Components, and the server needs to generate a complete HTML document. The <html> and <body> tags define the document structure. Without them, the server cannot produce valid HTML. Unlike the Pages Router (which uses _document.tsx for this purpose), the App Router consolidates everything into the root layout.
Key rules for the root layout:
- It cannot be a Client Component (no
'use client'directive) — but it can import and render client components - It must contain
<html>and<body>— no other layout should include these tags - It cannot use
headers(),cookies(), or other request-time APIs directly (but child server components can) - There is exactly one root layout per application (though route groups can create multiple root layouts for different sections)
Nested Layouts — Composing UI Layer by Layer
This is where layouts become truly powerful. Layouts nest automatically based on the folder structure. Each folder can have its own layout.tsx, and they compose from the outside in:
app/
layout.tsx --> Root layout (html, body, global nav)
page.tsx --> Home page
dashboard/
layout.tsx --> Dashboard layout (sidebar, dashboard nav)
page.tsx --> /dashboard
settings/
layout.tsx --> Settings layout (settings tabs)
page.tsx --> /dashboard/settings
profile/
page.tsx --> /dashboard/settings/profile
security/
page.tsx --> /dashboard/settings/security
When a user visits /dashboard/settings/profile, Next.js composes three layouts into a single view:
+----------------------------------------------------------+
| Root Layout (Header, Footer, Global Nav) |
| +-----------------------------------------------------+ |
| | Dashboard Layout (Sidebar, Dashboard Nav) | |
| | +------------------------------------------------+ | |
| | | Settings Layout (Settings Tabs) | | |
| | | +-------------------------------------------+ | | |
| | | | Profile Page (actual content) | | | |
| | | +-------------------------------------------+ | | |
| | +------------------------------------------------+ | |
| +-----------------------------------------------------+ |
+----------------------------------------------------------+
Each layout independently decides what UI to render, and each one persists independently. If the user navigates from /dashboard/settings/profile to /dashboard/settings/security:
- The Root Layout stays mounted (it wraps everything)
- The Dashboard Layout stays mounted (still in
/dashboard/*) - The Settings Layout stays mounted (still in
/dashboard/settings/*) - Only the page content swaps from Profile to Security
If the user navigates from /dashboard/settings/profile to /dashboard/analytics:
- The Root Layout stays mounted
- The Dashboard Layout stays mounted
- The Settings Layout unmounts (we left
/dashboard/settings/*) - The Analytics page mounts as a new child of the Dashboard Layout
Here is the nested layout code:
// app/dashboard/settings/layout.tsx
// This layout adds settings-specific tabs on top of the dashboard layout
// It persists when navigating between /dashboard/settings/* pages
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
{/* Settings-specific navigation tabs */}
<nav className="flex gap-4 border-b border-gray-700 pb-4 mb-6">
<a href="/dashboard/settings/profile"
className="text-emerald-400 hover:text-emerald-300">
Profile
</a>
<a href="/dashboard/settings/security"
className="text-gray-400 hover:text-gray-300">
Security
</a>
<a href="/dashboard/settings/notifications"
className="text-gray-400 hover:text-gray-300">
Notifications
</a>
</nav>
{/* Settings page content renders here */}
{children}
</div>
);
}
The nesting is automatic. You do not need to import parent layouts into child layouts. Next.js reads the file system and composes them for you. This is the "convention over configuration" philosophy in action.
template.js — The Fresh Start on Every Navigation
Now comes the counterpart: template.js. A template has exactly the same API as a layout — it receives {children} and wraps pages. But it behaves fundamentally differently:
A template re-mounts on every navigation. When a user navigates from /auth/login to /auth/register, a template wrapping the /auth route will:
- Unmount completely
- Mount a brand-new instance
- Reset all state to initial values
- Re-run all effects
- Replay all CSS animations
- Reset all uncontrolled form inputs
// app/auth/template.tsx
// This template re-mounts on every navigation within /auth/*
// Use this when you NEED a fresh instance each time
'use client';
import { useEffect, useState } from 'react';
export default function AuthTemplate({
children,
}: {
children: React.ReactNode;
}) {
const [fadeIn, setFadeIn] = useState(false);
// This effect runs on EVERY navigation because the template re-mounts
useEffect(() => {
// Trigger entrance animation every time the user visits a new auth page
setFadeIn(true);
// Log page view for analytics
console.log('Auth page viewed — fresh mount');
return () => {
// Cleanup runs when leaving each auth page
console.log('Auth page left — full unmount');
};
}, []);
return (
<div className={`transition-opacity duration-500 ${fadeIn ? 'opacity-100' : 'opacity-0'}`}>
<div className="max-w-md mx-auto mt-20 p-8 bg-gray-800 rounded-lg">
{children}
</div>
</div>
);
}
If you used a layout.js instead of template.js here, the fade-in animation would only play once — when the user first enters the /auth section. Subsequent navigations between login, register, and forgot-password would show no animation because the layout never re-mounts.
Layout vs Template — Side-by-Side Comparison
+----------------------------+---------------------------+---------------------------+
| Behavior | layout.js | template.js |
+----------------------------+---------------------------+---------------------------+
| Re-mounts on navigation? | No — persists | Yes — fresh instance |
| State preserved? | Yes | No — resets every time |
| useEffect re-runs? | No (same instance) | Yes (new instance) |
| CSS animations replay? | No | Yes |
| DOM re-created? | No | Yes |
| Receives {children}? | Yes | Yes |
| Can be nested? | Yes | Yes |
| Can coexist in same dir? | Yes — template wraps | Yes — layout wraps |
| | inside layout | around template |
+----------------------------+---------------------------+---------------------------+
When both exist in the same folder, the rendering order is:
Layout --> Template --> Page
// Conceptually:
<Layout>
<Template key={routeParam}>
<Page />
</Template>
</Layout>
The layout persists, but the template re-mounts (as if Next.js passes a changing key prop to the template based on the route).
When to Use template.js
Templates are less common than layouts. Most of the time, you want persistence — it is more performant and provides a smoother user experience. But there are specific scenarios where a fresh mount is exactly what you need:
1. Entrance and exit animations: If you want a fade-in, slide-in, or any CSS/Framer Motion animation to replay every time the user navigates to a new page within a section, you need a template. Layouts won't replay animations because the DOM is preserved.
2. Per-page analytics logging via useEffect:
If you log page views using useEffect and want the effect to fire on every navigation within a section, a template guarantees it. In a layout, useEffect only fires once when the layout first mounts.
3. Resetting form state: If a section has a shared form shell (like a multi-step wizard where each step is a different route), and you want uncontrolled inputs to reset when moving between steps, a template gives you a clean slate.
4. Feature-flag or A/B test wrappers: If you want to re-evaluate a feature flag or randomize a variant on every navigation, a template ensures the evaluation happens fresh each time.
// app/onboarding/template.tsx
// Each onboarding step gets a fresh animation and progress reset
'use client';
import { motion } from 'framer-motion';
export default function OnboardingTemplate({
children,
}: {
children: React.ReactNode;
}) {
return (
<motion.div
// Because this is a template, framer-motion re-mounts
// and plays the animation on every step transition
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
Sharing State via Layout — No Prop Drilling Needed
One of the most powerful patterns with layouts is sharing state across all pages within a route segment without prop drilling. Since the layout persists and wraps all child pages, you can place context providers inside a layout and every nested page automatically has access to that context.
// app/dashboard/layout.tsx
// The layout provides shared state to all dashboard pages via React Context
import { DashboardProvider } from './dashboard-context';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DashboardProvider>
<div className="flex min-h-screen">
<Sidebar />
<main className="flex-1 p-8">
{children}
</main>
</div>
</DashboardProvider>
);
}
// app/dashboard/dashboard-context.tsx
'use client';
import { createContext, useContext, useState } from 'react';
type DashboardContextType = {
sidebarOpen: boolean;
toggleSidebar: () => void;
selectedTeam: string;
setSelectedTeam: (team: string) => void;
};
const DashboardContext = createContext<DashboardContextType | null>(null);
export function DashboardProvider({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [selectedTeam, setSelectedTeam] = useState('default');
return (
<DashboardContext.Provider
value={{
sidebarOpen,
toggleSidebar: () => setSidebarOpen(prev => !prev),
selectedTeam,
setSelectedTeam,
}}
>
{children}
</DashboardContext.Provider>
);
}
export function useDashboard() {
const context = useContext(DashboardContext);
if (!context) {
throw new Error('useDashboard must be used within a DashboardLayout');
}
return context;
}
// app/dashboard/analytics/page.tsx
// This page accesses the shared dashboard state without any prop drilling
'use client';
import { useDashboard } from '../dashboard-context';
export default function AnalyticsPage() {
const { selectedTeam, sidebarOpen } = useDashboard();
return (
<div>
<h1>Analytics for {selectedTeam}</h1>
<p>Sidebar is {sidebarOpen ? 'open' : 'closed'}</p>
{/* Analytics content */}
</div>
);
}
Because the layout never re-mounts, the DashboardProvider stays alive and its state persists. When the user toggles the sidebar on the settings page and then navigates to analytics, the sidebar stays in its toggled state. This is the power of layout persistence combined with React Context.
Important caveat: You cannot pass data directly from a layout to its {children} via props. The children prop is a ReactNode, not a function. The two recommended patterns for sharing data between a layout and its pages are:
- React Context (as shown above) — best for client-side state
- Shared data fetching patterns — fetch in the layout (Server Component), and let child pages fetch the same data (Next.js deduplicates identical
fetchcalls automatically)
Common Mistakes
1. Adding <html> and <body> tags in nested layouts.
Only the root layout should contain <html> and <body>. If you add them in a nested layout, you'll get duplicate document tags, which creates invalid HTML and causes hydration errors. This is one of the most common mistakes when developers first work with nested layouts.
2. Expecting layout.js to re-run useEffect on navigation.
Because layouts persist, useEffect inside a layout only runs when the layout first mounts. If you need an effect to run on every child page navigation, either move the effect to the page level, use a template instead, or listen to route changes using usePathname() inside a useEffect dependency array.
3. Using template.js when layout.js would work. Templates re-mount on every navigation, which means they destroy and recreate DOM nodes, re-run effects, and reset state. This is more expensive than letting a layout persist. Only use templates when you specifically need the re-mount behavior (animations, effect re-runs, state resets). The default choice should always be layout.
4. Trying to pass props from layout to children directly.
You cannot do {children(someData)} because children is a ReactNode, not a render function. Use React Context or parallel data fetching instead. Many developers try to pass fetched data from a server layout to its pages — this is not how the API works.
5. Forgetting that layouts cannot access the current pathname on the server.
A server layout does not receive the current URL or route params of its child pages. If you need the pathname in a layout, you must either make part of it a Client Component and use usePathname(), or restructure so the route segment information is available from params in the specific layout's own route segment.
Interview Questions
1. "What is the difference between layout.js and template.js in Next.js?"
layout.js persists across navigations — it mounts once and stays alive as long as the user stays within its route segment. State is preserved, effects don't re-run, and DOM elements are not recreated. template.js re-mounts on every navigation — it creates a fresh component instance each time, resetting all state, re-running all effects, and replaying animations. Both receive {children} and wrap pages, but the lifecycle behavior is opposite. When both exist in the same folder, the layout wraps the template: Layout > Template > Page. You should default to layout.js for performance and only reach for template.js when you specifically need fresh-mount behavior like entrance animations or per-navigation analytics logging.
2. "Why does Next.js require a root layout? Can you have multiple root layouts?"
Next.js requires a root layout because the App Router uses React Server Components to generate complete HTML documents. The root layout must contain <html> and <body> tags — these define the document structure that the server sends to the browser. Without them, Next.js cannot produce valid HTML. You can technically have multiple root layouts by using route groups: app/(marketing)/layout.tsx and app/(dashboard)/layout.tsx can each be root layouts with their own <html> and <body> tags. This is useful when completely different sections of your app need different fonts, metadata, or document-level configuration. But each route group must have its own root layout — you cannot share one across groups in this pattern.
3. "How would you share state across multiple pages that share the same layout?"
The recommended approach is React Context. Place a context provider inside the layout, and all pages under that layout segment can access the context via a custom hook. Because the layout persists across navigation, the context provider stays mounted and state is preserved. For example, a dashboard layout might provide sidebar state, selected team, or notification count to all dashboard pages. For server-side data, Next.js automatically deduplicates identical fetch calls, so both the layout and child pages can fetch the same endpoint without triggering redundant network requests — the result is cached and reused within the same render pass.
4. "A user navigates from /dashboard/settings to /dashboard/analytics. Which components re-mount and which persist?"
The root layout persists — it always does. The dashboard layout persists because both routes are under /dashboard. The settings page unmounts completely, and the analytics page mounts fresh. If there was a settings-specific layout at app/dashboard/settings/layout.tsx, that unmounts too, because we left the /dashboard/settings segment. Only layouts that are shared between the source and destination routes persist. Everything below the first divergence point in the route tree is unmounted and remounted.
5. "Give me a real-world scenario where you'd use template.js instead of layout.js."
An onboarding flow with animated page transitions. Suppose you have /onboarding/step-1, /onboarding/step-2, and /onboarding/step-3. You want each step to slide in from the right with a Framer Motion animation. If you use a layout, the animation only plays once when the user enters the onboarding section — subsequent step transitions show no animation because the layout never re-mounts. With a template, every navigation triggers a fresh mount, so the slide-in animation replays on each step. Another common example is per-page analytics: if you need useEffect to fire on every sub-page navigation to log a view event, a template guarantees the effect runs because it creates a new component instance each time.
Quick Reference — Layouts & Templates Cheat Sheet
| Question | layout.js | template.js |
|---|---|---|
| Re-mounts on navigation? | No | Yes |
| State preserved? | Yes | No (resets) |
| useEffect re-runs? | No | Yes |
| CSS/Framer animations replay? | No | Yes |
| Performance cost | Lower (persists) | Higher (re-mounts) |
| Must include html/body? | Only root layout | Never |
| Can nest? | Yes | Yes |
| Default choice? | Yes | Only when needed |
| Wrapping order | Outer | Inner (Layout > Template > Page) |
| Best for | Nav, sidebars, providers | Animations, analytics, form resets |
The one-sentence rule: Use layout.js by default for persistent UI shells, and only reach for template.js when you specifically need a fresh component instance on every navigation.
+----------------------------------------------------------+
| THE LAYOUT & TEMPLATE DECISION |
+----------------------------------------------------------+
| |
| Need persistent state, sidebar, nav? |
| --> layout.js |
| |
| Need animations to replay on every navigation? |
| --> template.js |
| |
| Need useEffect to fire on every sub-navigation? |
| --> template.js |
| |
| Need a shared context provider for child pages? |
| --> layout.js (provider stays mounted) |
| |
| Unsure? |
| --> layout.js (it's the default for a reason) |
| |
+----------------------------------------------------------+
Prev: Lesson 5.1 -- Dynamic Routes & Catch-All Segments Next: Lesson 5.3 -- Parallel & Intercepting Routes
This is Lesson 5.2 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.