Next.js Layout System — Nested Layouts, Templates, and Shared UI
Advertisement
Introduction
The Next.js layout system is built on a simple rule: layout.tsx files wrap their sibling page.tsx and all child routes, and they do NOT re-render on navigation between child routes. This makes them perfect for persistent UI like sidebars, navigation bars, and authentication wrappers. template.tsx is a variant that does re-mount on every navigation — useful for page transitions and analytics.
Why This Matters
In the Pages Router, every page imported its own layout, making it difficult to share state or keep components mounted across navigations. The App Router solves this fundamentally: layouts are persistent. A sidebar component inside a layout stays mounted as users navigate between child pages — no flicker, no re-fetch, no lost scroll position.
This matters for user experience. A music player that continues playing while users browse, a sidebar search that stays populated, or a filtered list that persists across sub-pages — all of these are easy with the nested layout system and impossible without it.
Root Layout — Required
Every App Router project requires a root layout at app/layout.tsx. It must include html and body tags and applies to every page:
// app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'The best app for managing your workflow.',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<header className="border-b px-6 py-4">
<nav>
<a href="/" className="font-bold">My App</a>
</nav>
</header>
{children}
<footer className="border-t px-6 py-8 text-sm text-gray-500">
© 2026 My App
</footer>
</body>
</html>
)
}Nested Layouts
Add layout.tsx inside any subfolder to create a nested layout. It wraps only that section:
// app/dashboard/layout.tsx — applies to /dashboard and all sub-routes
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { DashboardNav } from '@/components/DashboardNav'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r bg-gray-50">
<DashboardNav user={session.user} />
</aside>
<main className="flex-1 p-8">{children}</main>
</div>
)
}This layout is a great place to put authentication checks — they run once and protect all dashboard routes.
Layout Composition
Layouts compose automatically. Navigating to /dashboard/analytics renders three layouts:
RootLayout (app/layout.tsx)
└── DashboardLayout (app/dashboard/layout.tsx)
└── page.tsx (app/dashboard/analytics/page.tsx)The root layout header and footer are always visible. The dashboard sidebar is always visible. Only the page content changes.
Passing Data from Layout to Children
Layouts cannot pass props directly to pages. Use one of these patterns:
Option 1: Shared data layer (recommended)
// lib/getUser.ts — shared by layout and page
import { cache } from 'react'
import { auth } from '@/lib/auth'
export const getUser = cache(async () => {
const session = await auth()
return session?.user ?? null
})// app/dashboard/layout.tsx
import { getUser } from '@/lib/getUser'
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const user = await getUser() // Cached — page can call this too
return (
<div>
<aside><UserAvatar user={user} /></aside>
<main>{children}</main>
</div>
)
}Option 2: Context from a Client Component wrapper
// components/SessionProvider.tsx
'use client'
import { createContext, useContext } from 'react'
// ... context implementationtemplate.tsx — Re-mounting Layouts
template.tsx is identical to layout.tsx in its placement and props, but it re-mounts on every navigation to child routes. Use it when you need:
- Page transition animations (mount/unmount triggers CSS transitions)
useEffectthat runs on every page navigation- Re-initializing analytics or form state per page
// app/blog/template.tsx
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
export default function BlogTemplate({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
useEffect(() => {
// Fires on every navigation within /blog
console.log('Navigated to:', pathname)
window.analytics?.page({ path: pathname })
}, [pathname])
return (
<div className="animate-fade-in">
{children}
</div>
)
}Multiple Root Layouts with Route Groups
Use route groups to apply different root layouts to different sections of your app — useful for a marketing site and a separate dashboard with distinct HTML <head> configurations:
app/
├── (marketing)/
│ ├── layout.tsx ← marketing layout (light theme, public nav)
│ ├── page.tsx → /
│ └── about/
│ └── page.tsx → /about
└── (app)/
├── layout.tsx ← app layout (dark theme, sidebar)
└── dashboard/
└── page.tsx → /dashboard// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="light">
<body>
<PublicNav />
{children}
<PublicFooter />
</body>
</html>
)
}// app/(app)/layout.tsx
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body>
<AppSidebar />
<main>{children}</main>
</body>
</html>
)
}Note: When using multiple root layouts in route groups, the top-level app/layout.tsx should NOT exist, or it will conflict.
Layout with Providers
Wrap the entire app with context providers in the root layout:
// app/layout.tsx
import { ThemeProvider } from '@/components/providers/ThemeProvider'
import { CartProvider } from '@/components/providers/CartProvider'
import { QueryProvider } from '@/components/providers/QueryProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ThemeProvider>
<QueryProvider>
<CartProvider>
{children}
</CartProvider>
</QueryProvider>
</ThemeProvider>
</body>
</html>
)
}Each provider is a Client Component, but they wrap Server Component children without issues.
Common Mistakes
- Adding
'use client'tolayout.tsxunnecessarily — this makes the entire layout client-side - Trying to pass props from layout to page directly — layouts cannot do this; use shared data layers
- Using
template.tsxwhenlayout.tsxis needed — templates re-mount on every navigation - Forgetting that layouts at the same level do NOT catch errors from their own
layout.tsx - Creating layouts that fetch data sequentially when
Promise.allwould parallelize it
Best Practices
- Keep root layout as a Server Component — add interactivity via Client Component wrappers inside
- Use layouts for auth checks — one check protects all child routes without per-page repetition
- Use
template.tsxonly when you specifically need re-mounting behavior (transitions, analytics) - Use route groups with separate layouts to serve radically different sections of your app
- Use
cache()from React for shared data fetching between layouts and pages
Key Takeaways
layout.tsxpersists across child route navigations — it does NOT re-mount on every page changetemplate.tsxis identical to layout but re-mounts on every navigation — use for transitions- The root
app/layout.tsxis required and must includehtmlandbodytags - Nested layouts compose automatically — multiple layout.tsx files stack from root to leaf
- Layouts cannot pass props to pages — use shared data layers with
cache()or React Context - Auth checks in a layout protect all child routes without repeating the check per page
- Route groups allow multiple root layouts at the top level for distinct app sections
- Layouts are Server Components by default — only add
'use client'if you need browser APIs
Advertisement