Overview
Next.js App Router provides two powerful features for creating consistent UI patterns: layouts and templates. Understanding the difference between them is crucial for building maintainable applications.
Layouts Layouts are shared UI that persists across route changes and maintains state during navigation.
Layout Characteristics Shared UI across multiple pages Maintains state during navigation Renders once and stays mounted Perfect for navigation, headers, footers
Layout Benefits Consistent user experience Performance optimization State preservation Reduced re-rendering Templates Templates create a new instance for each route and re-render on every navigation.
Template Characteristics Creates new instance for each route Re-renders on every navigation Perfect for animations and transitions Useful for enter/exit effects
Template Benefits Page transition animations Fresh state for each route Custom enter/exit effects Route-specific styling
Root Layout The root layout wraps all pages and is required in every Next.js app. // 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: "My App" ,
description: "Generated by create next app" ,
};
export default function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< html lang = "en" className = {inter. className } >
< body >
< header >
< nav >
< a href = "/" > Home </ a >
< a href = "/about" > About </ a >
< a href = "/contact" > Contact </ a >
</ nav >
</ header >
< main >{ children } </ main >
< footer >
< p > & copy ; 2024 My App . All rights reserved . </ p >
</ footer >
</ body >
</ html >
);
}
Nested Layouts Layouts can be nested to create hierarchical UI structures. // app/dashboard/layout.tsx
import Sidebar from "@/components/Sidebar" ;
export default function DashboardLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< div className = "dashboard-layout" >
< Sidebar />
< div className = "dashboard-content" > { children } </ div >
</ div >
);
}
Layout Hierarchy app/
├── layout.tsx # Root layout (wraps everything)
├── dashboard/
│ ├── layout.tsx # Dashboard layout (wraps dashboard pages)
│ ├── page.tsx # /dashboard
│ └── settings/
│ ├── layout.tsx # Settings layout (wraps settings pages)
│ └── page.tsx # /dashboard/settings
Basic Template // app/template.tsx
"use client" ;
import { useEffect , useState } from "react" ;
import { usePathname } from "next/navigation" ;
export default function Template ({ children } : { children : React . ReactNode }) {
const [ isLoading , setIsLoading ] = useState ( false );
const pathname = usePathname ();
useEffect (() => {
setIsLoading ( true );
const timer = setTimeout (() => setIsLoading ( false ), 300 );
return () => clearTimeout ( timer );
}, [ pathname ]);
return (
< div
className = { `transition-opacity duration-300 ${
isLoading ? "opacity-0" : "opacity-100"
} ` } >
{ children }
</ div >
);
}
Animated Template // app/template.tsx
"use client" ;
import { motion } from "framer-motion" ;
import { usePathname } from "next/navigation" ;
export default function AnimatedTemplate ({
children ,
} : {
children : React . ReactNode ;
}) {
const pathname = usePathname ();
return (
< motion . div
key = { pathname }
initial = {{ opacity : 0 , y : 20 }}
animate = {{ opacity : 1 , y : 0 }}
exit = {{ opacity : 0 , y : - 20 }}
transition = {{ duration : 0.3 }} >
{ children }
</ motion . div >
);
}
Authentication Layout // app/(auth)/layout.tsx
import Image from "next/image" ;
import logo from "@/public/logo.png" ;
export default function AuthLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< div className = "min-h-screen flex items-center justify-center bg-gray-50" >
< div className = "max-w-md w-full space-y-8" >
< div className = "text-center" >
< Image
src = { logo }
alt = "Logo"
width = { 80 }
height = { 80 }
className = "mx-auto"
/>
< h2 className = "mt-6 text-3xl font-extrabold text-gray-900" >
Welcome Back
</ h2 >
</ div >
< div className = "bg-white py-8 px-6 shadow rounded-lg" > { children } </ div >
</ div >
</ div >
);
}
Admin Layout // app/admin/layout.tsx
import AdminSidebar from "@/components/AdminSidebar" ;
import AdminHeader from "@/components/AdminHeader" ;
export default function AdminLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< div className = "admin-layout" >
< AdminSidebar />
< div className = "admin-main" >
< AdminHeader />
< div className = "admin-content" > { children } </ div >
</ div >
</ div >
);
}
E-commerce Layout // app/shop/layout.tsx
import ShopHeader from "@/components/ShopHeader" ;
import ShopFooter from "@/components/ShopFooter" ;
import CartSidebar from "@/components/CartSidebar" ;
export default function ShopLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< div className = "shop-layout" >
< ShopHeader />
< main className = "shop-main" > { children } </ main >
< ShopFooter />
< CartSidebar />
</ div >
);
}
Loading Templates // app/loading.tsx
export default function Loading () {
return (
< div className = "flex items-center justify-center min-h-screen" >
< div className = "animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500" > </ div >
</ div >
);
}
Error Boundaries // app/error.tsx
"use client" ;
import { useEffect } from "react" ;
export default function Error ({
error ,
reset ,
} : {
error : Error & { digest ?: string };
reset : () => void ;
}) {
useEffect (() => {
console . error ( error );
}, [ error ]);
return (
< div className = "error-boundary" >
< h2 > Something went wrong !</ h2 >
< button onClick = { reset } > Try again </ button >
</ div >
);
}
Not Found Pages // app/not-found.tsx
import Link from "next/link" ;
export default function NotFound () {
return (
< div className = "not-found" >
< h2 > Not Found </ h2 >
< p > Could not find requested resource </ p >
< Link href = "/" > Return Home </ Link >
</ div >
);
}
Conditional Layouts // app/layout.tsx
"use client" ;
import { usePathname } from "next/navigation" ;
import AuthLayout from "@/components/AuthLayout" ;
import MainLayout from "@/components/MainLayout" ;
export default function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
const pathname = usePathname ();
const isAuthPage = pathname . startsWith ( "/auth" );
if ( isAuthPage ) {
return < AuthLayout >{ children } </ AuthLayout > ;
}
return < MainLayout >{ children } </ MainLayout > ;
}
Dynamic Layouts // app/layout.tsx
import { getLayout } from "@/lib/layouts" ;
export default async function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
const Layout = await getLayout ();
return < Layout >{ children } </ Layout > ;
}
Layout with Context // app/dashboard/layout.tsx
"use client" ;
import { createContext , useContext , useState } from "react" ;
const DashboardContext = createContext ({
sidebarOpen: false ,
setSidebarOpen : ( open : boolean ) => {},
});
export const useDashboard = () => useContext ( DashboardContext );
export default function DashboardLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
const [ sidebarOpen , setSidebarOpen ] = useState ( false );
return (
< DashboardContext . Provider value = {{ sidebarOpen , setSidebarOpen }} >
< div className = "dashboard-layout" >
< Sidebar />
< main className = "dashboard-main" > { children } </ main >
</ div >
</ DashboardContext . Provider >
);
}
Layout Design
Keep Layouts Simple
Focus on shared UI elements and avoid complex logic
Use Nested Layouts
Create hierarchical layouts for better organization
Optimize Performance
Minimize re-renders and use proper state management
Handle Loading States
Provide appropriate loading and error states
Template Usage
Use for Animations
Templates are perfect for page transition animations
Fresh State
Use when you need fresh state for each route
Enter/Exit Effects
Implement custom enter and exit animations
Performance Consideration
Be mindful of re-rendering costs
Responsive Layouts // app/layout.tsx
export default function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< html lang = "en" >
< body >
< div className = "min-h-screen flex flex-col" >
< header className = "bg-white shadow-sm" >
< nav className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" >
{ /* Navigation content */ }
</ nav >
</ header >
< main className = "flex-1" > { children } </ main >
< footer className = "bg-gray-800 text-white" >
< div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" >
{ /* Footer content */ }
</ div >
</ footer >
</ div >
</ body >
</ html >
);
}
// app/blog/layout.tsx
import { Metadata } from "next" ;
export const metadata : Metadata = {
title: "Blog - My App" ,
description: "Read our latest blog posts" ,
};
export default function BlogLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< div className = "blog-layout" >
< aside className = "blog-sidebar" >
< h2 > Categories </ h2 >
{ /* Category links */ }
</ aside >
< main className = "blog-main" > { children } </ main >
</ div >
);
}
Problem : Layout styles not showing on child routes
Solution : Check that layout.tsx is in the correct directory and wraps children properly
Problem : State is lost when navigating between routes Solution : Ensure
you’re using layouts (not templates) for state that should persist
Template Re-rendering Issues
Problem : Template is causing performance issues Solution : Consider
using layouts instead, or optimize the template logic
Problem : Nested layouts are conflicting with each other
Solution : Review the layout hierarchy and ensure proper nesting
Key Takeaway : Layouts and templates are powerful tools for creating
consistent UI patterns. Use layouts for shared UI that should persist across
routes, and templates for animations and fresh state on each navigation.