In enterprise-scale web applications, pages share structural scaffolding: navigation bars, collapsable sidebars, authenticated profile widgets, global audio players, and persistent footers. In traditional client-side client-side SPAs, maintaining these shells across page transitions created systemic architectural friction:
The Next.js App Router resolves these challenges through a strict division of responsibilities between Layouts and Templates.
Layouts preserve mounted component instances: on transitions between child routes, layouts never unmount, preserving input focus, local React state, and omitting redundant server data fetching.
Templates recreate themselves from scratch: on every URL mutation, the prior template instance is completely unmounted, clearing state and re-triggering entrance animations and lifecycle effects.
In the React Fiber architecture, a layout (layout.tsx) represents a stable virtual DOM node whose local state, DOM elements, and focus states persist. Child routes are passed into the layout strictly through the children prop. When an inner route changes, React Fiber performs Subtree Reconciliation: only the inner contents of children update, while the layout node remains untouched.
The layout hierarchy mirrors the filesystem structure:
template.tsx) compile as direct children of the corresponding segment layout, but are wrapped in a dynamic key={currentSegmentId}, triggering full remounting during navigation.Click different routes to observe Preserved State isolation in nested layouts
Switch routes: this text will NOT reset!
Navigating inside the dashboard: DashboardLayout is NOT remounted! Sidebar input text is fully preserved.
The execution lifecycle of layouts and templates during route transitions is governed by Fiber Reconciliation phases:
The user clicks a link navigating from /dashboard/orders to /dashboard/analytics.
React compares segments and identifies app/dashboard/layout.tsx as a shared ancestor. Its mounted Fiber instance is preserved.
Next.js updates the Template wrapper key (key='analytics'). The prior template node unmounts, clearing local state.
React instantiates a fresh Template and child Page, re-running entrance animations and pageview telemetry hooks.
The reconciled sub-tree is injected directly into the parent layout's children slot without re-rendering the surrounding UI shell.
In a sidebar inside app/dashboard/layout.tsx, a user types an active query into a search input, then clicks a navigation link to another child page. What happens to the text in the input?
The Root Layout is defined in app/layout.tsx and is mandatory for all Next.js applications. It is the only component permitted to render top-level <html> and <body> tags.
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: {
template: '%s | Fullstack Agentic Lab',
default: 'Fullstack Agentic Lab',
},
description: 'Fullstack and AI Engineering Education Platform',
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" className="dark">
<body className="min-h-screen bg-bg-base text-text-primary antialiased">
<header className="border-b border-border p-4">Global Navigation Shell</header>
<main>{children}</main>
</body>
</html>
);
}Starting with Next.js 15, when a layout resides inside a dynamic folder such as app/[tenant]/layout.tsx, the params prop is received as a Promise:
interface TenantLayoutProps {
children: React.ReactNode;
params: Promise<{ tenant: string }>;
}
export default async function TenantLayout({ children, params }: TenantLayoutProps) {
// Asynchronously resolve route parameters inside the layout
const { tenant } = await params;
return (
<div className="tenant-container flex min-h-screen">
<aside className="tenant-sidebar w-64 border-r border-border p-4">
<h2 className="text-lg font-bold">Organization: {tenant}</h2>
</aside>
<section className="tenant-content flex-1 p-6">
{children}
</section>
</div>
);
}Unlike page.tsx, server-side layout.tsx components do not receive the searchParams prop.
This design choice is essential for caching efficiency:
?page=2&sort=asc), the server would have to re-render the entire parent shell on every pagination or filter update.searchParams allows Next.js to retain them in the browser's in-memory Client Router Cache throughout the user's session.If a client component inside a layout requires access to query parameters, consume the client-side hook useSearchParams() from next/navigation.
Next.js compiles template.tsx as an immediate wrapper around the page within the layout's children slot:
<Layout>
{/* The Template is keyed with the dynamic current segment ID */}
<Template key={currentSegmentId}>
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
</ErrorBoundary>
</Template>
</Layout>When currentSegmentId shifts from orders to analytics, the mutated key instructs React Fiber: "Unmount the prior DOM node and mount a completely fresh instance from scratch."
You must implement a CSS fade-in entrance animation whenever users navigate between sections of an administrative portal, while also dispatching a page_view analytics event. Which file should you use?
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
interface TemplateProps {
children: React.ReactNode;
}
export default function DashboardTemplate({ children }: TemplateProps) {
const pathname = usePathname();
useEffect(() => {
// Dispatches pageview telemetry reliably on every route transition
console.log(`[Telemetry] Page view registered: ${pathname}`);
}, [pathname]);
return (
<div className="animate-in fade-in duration-300">
{children}
</div>
);
}A common requirement is mounting a client-side React Context provider (e.g., Theme, Locale) in the root layout without turning the entire layout into a Client Component:
'use client';
import { createContext, useContext, useState } from 'react';
type Theme = 'dark' | 'light';
const ThemeContext = createContext<{
theme: Theme;
toggleTheme: () => void;
}>({
theme: 'dark',
toggleTheme: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('dark');
const toggleTheme = () => {
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);import { ThemeProvider } from '@/components/providers/ThemeProvider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{/* ThemeProvider is a client boundary, but {children} is passed as server-rendered RSC without bundle bloat! */}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
interface AdminLayoutProps {
children: React.ReactNode;
}
export default async function AdminLayout({ children }: AdminLayoutProps) {
// Asynchronous cookies reading in Next.js 15+
const cookieStore = await cookies();
const sessionToken = cookieStore.get('auth_token')?.value;
if (!sessionToken) {
redirect('/login?from=/admin');
}
return (
<div className="admin-shell min-h-screen bg-bg-surface">
<header className="border-b border-border p-4 font-bold">
Admin Portal
</header>
<main className="p-6">{children}</main>
</div>
);
}Developers often attempt to access query parameters via props.searchParams inside server layouts, only to find the value is always undefined.
Symptom: Query filters or pagination parameters evaluate to undefined inside layouts.
Cause: Server layouts are intentionally decoupled from searchParams to enable long-term browser Router Cache retention.
Resolution: Read query parameters in server page.tsx, or consume useSearchParams() inside an isolated client leaf component.
Adding 'use client' to app/layout.tsx marks the entire application tree below it as Client Components, blocking server streaming and inflating the browser bundle.
Symptom: Server-only operations fail in child components, and client JavaScript bundle size spikes.
Cause: The 'use client' directive defines a serialization boundary that waterfalls through all imported children.
Resolution: Maintain the Root Layout as a Server Component. Encapsulate client providers in leaf components and pass server pages via {children}.
Unlike the legacy Pages Router where _document.tsx silently supplied base document tags, the App Router app/layout.tsx must explicitly render them.
Symptom: Fatal Turbopack compilation crash: "Root layout must contain <html> and <body> tags."
Resolution: Ensure the uppermost layout outputs standard <html> and <body> tags.