REACT & NEXT.JS / 5. APP ROUTER & LAYOUTS

Next.js App Router — Layouts, Pages, Loading, Error

File-based routing with nested layouts — the backbone of your app


EXPLANATION

The App Router uses the filesystem as the router. Every folder = route segment, every page.tsx = route.

Special files:
• page.tsx      → the UI for a route, makes it publicly accessible
• layout.tsx    → wraps page and all children (persists across navigation!)
• loading.tsx   → shown instantly while page.tsx is loading (Suspense boundary)
• error.tsx     → shown when page.tsx throws (Error boundary, must be client)
• not-found.tsx → shown when notFound() is called
• route.ts      → API endpoint (no UI)

Dynamic routes:
• [slug]        → /blog/my-post → params.slug = "my-post"
• [...slug]     → /docs/a/b/c  → params.slug = ["a","b","c"]
• (group)       → route group, doesn't affect URL

Layouts are the key feature: they don't unmount when you navigate between pages. This is what makes instant navigation feel smooth — your sidebar and navbar stay mounted.

generateStaticParams() → tells Next.js which dynamic routes to pre-render at build time.

DIAGRAM

app/
  ├── layout.tsx          → wraps EVERYTHING (html, body)
  ├── page.tsx            → route: /
  ├── (marketing)/        → route group (no URL segment)
  │   ├── about/page.tsx  → route: /about
  │   └── blog/
  │       ├── layout.tsx  → wraps all /blog/* pages
  │       ├── page.tsx    → route: /blog
  │       └── [slug]/
  │           ├── page.tsx    → route: /blog/:slug
  │           └── loading.tsx → shown while page loads
  └── dashboard/
      ├── layout.tsx      → sidebar layout for dashboard
      ├── page.tsx        → route: /dashboard
      └── settings/
          └── page.tsx    → route: /dashboard/settings

  Navigation: layout.tsx stays mounted, only page.tsx swaps

CODE

TSX
1// ── Root layout (app/layout.tsx) ─────────────────────────────────
2import type { Metadata } from "next";
3import { Inter } from "next/font/google";
4
5const font = Inter({ subsets: ["latin"] });
6
7export const metadata: Metadata = {
8 title: { template: "%s | deepdocs", default: "deepdocs" },
9 description: "Personal AI/ML learning docs",
10};
11
12export default function RootLayout({ children }: { children: React.ReactNode }) {
13 return (
14 <html lang="en">
15 <body className={font.className}>
16 {children}
17 </body>
18 </html>
19 );
20}
21
22// ── Nested layout (app/dashboard/layout.tsx) ─────────────────────
23export default function DashboardLayout({ children }: { children: React.ReactNode }) {
24 return (
25 <div className="flex h-screen">
26 <Sidebar /> {/* persists across dashboard pages */}
27 <main className="flex-1 overflow-auto p-6">
28 {children}
29 </main>
30 </div>
31 );
32}
33
34// ── Page with metadata (app/blog/[slug]/page.tsx) ─────────────────
35type Props = { params: Promise<{ slug: string }> };
36
37export async function generateMetadata({ params }: Props) {
38 const { slug } = await params;
39 const post = await getPost(slug);
40 return { title: post.title, description: post.excerpt };
41}
42
43export async function generateStaticParams() {
44 const posts = await getAllPosts();
45 return posts.map(post => ({ slug: post.slug })); // pre-render these at build time
46}
47
48export default async function BlogPost({ params }: Props) {
49 const { slug } = await params;
50 const post = await getPost(slug);
51
52 return (
53 <article>
54 <h1>{post.title}</h1>
55 <p>{post.content}</p>
56 </article>
57 );
58}
59
60// ── loading.tsx (automatic Suspense boundary) ─────────────────────
61export default function Loading() {
62 return <div className="animate-pulse">Loading post...</div>;
63}
64
65// ── error.tsx (must be "use client") ─────────────────────────────
66"use client";
67export default function Error({ error, reset }: { error: Error; reset: () => void }) {
68 return (
69 <div>
70 <h2>Something went wrong</h2>
71 <p>{error.message}</p>
72 <button onClick={reset}>Try again</button>
73 </div>
74 );
75}
76
77// ── not-found.tsx ─────────────────────────────────────────────────
78export default function NotFound() {
79 return <h2>404 Page not found</h2>;
80}
81
82// Trigger from page: import { notFound } from "next/navigation"
83// if (!post) notFound();
84
85async function getPost(slug: string) { return { title: "", excerpt: "", content: "", slug }; }
86async function getAllPosts() { return [{ slug: "hello" }]; }
87function Sidebar() { return <aside>Sidebar</aside>; }
← PREV4. Context & State ManagementNEXT →6. Server vs Client Components