NextjsLayouts & Shared UI

Layouts & Shared UI

A layout is UI that is shared between multiple pages. When a user navigates between pages that share a layout, the layout preserves state, stays interactive, and does not re-render — only the page content below it swaps out. This is one of the most useful performance characteristics of the App Router.

Defining a Layout

A layout is defined by default-exporting a React component from a file named layout.tsx. Every layout must accept and render a children prop — that's where Next.js will inject the matching page or nested layout.

TSX
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard-shell">
      <nav>Dashboard Nav</nav>
      <main>{children}</main>
    </div>
  )
}

Every page inside app/dashboard now automatically renders wrapped in this shell — you never import or wire it up manually.

The Root Layout Is Required

Every Next.js App Router project must have a root layout at app/layout.tsx. Unlike every other layout in the tree, the root layout must render the html and body tags itself — Next.js does not add them for you.

TSX
// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
Warning
Forgetting html and body in the root layout, or adding them again in a nested layout, will break your app. The root layout is the only layout that should ever render these tags.
Layouts Don't Re-render on Navigation

This is the key behavioral difference from a page. If a user moves from /dashboard/overview to /dashboard/settings, and both live under the same app/dashboard/layout.tsx, React preserves that layout's component instance. Open accordions, scroll position, form input, and any local state inside the layout survive the navigation untouched — only the page content re-renders.

TSX
// app/dashboard/layout.tsx
'use client'
import { useState } from 'react'

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  const [collapsed, setCollapsed] = useState(false)

  return (
    <div>
      <button onClick={() => setCollapsed((c) => !c)}>Toggle sidebar</button>
      {!collapsed && <aside>Sidebar</aside>}
      <main>{children}</main>
    </div>
  )
}
// The collapsed state above survives navigation between any pages under /dashboard.
Tip
This makes layouts the ideal place for persistent navigation bars, sidebars, tab strips, or any chrome that should feel stable rather than flicker on every route change.
Layouts Cannot Access Route-Specific Data Directly

Layouts do not receive searchParams as a prop — only pages do. This is intentional: since a layout is shared across many pages (and doesn't re-render on sibling navigation), it shouldn't depend on data that changes per-page, or it would need to re-render on every navigation, defeating the purpose.

What Layouts Are Good For
  • Persistent navigation — headers, sidebars, tab bars, breadcrumb shells.

  • Shared providers and context that many pages in a section need.

  • Consistent page chrome (padding, max-width containers, section headings).

  • Fetching data that is shared by every page under that route segment.

Note
Layouts can be Server Components by default and can fetch their own data with async/await, just like pages. Only add 'use client' when the layout needs interactivity, state, or browser-only APIs.

Layouts are one of the App Router's biggest structural upgrades: shared UI is no longer something you hand-wire into every page — it's a natural consequence of where you place a layout.tsx file in the folder tree.