Next.js Layout System — Nested Layouts, Templates, and Shared UI

Sanjeev SharmaSanjeev Sharma
7 min read

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 implementation

template.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)
  • useEffect that 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' to layout.tsx unnecessarily — 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.tsx when layout.tsx is 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.all would 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.tsx only 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.tsx persists across child route navigations — it does NOT re-mount on every page change
  • template.tsx is identical to layout but re-mounts on every navigation — use for transitions
  • The root app/layout.tsx is required and must include html and body tags
  • 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading