Learn / Frameworks / Next.js / Routing, Layouts and Navigation

Beginner 15 min

Routing, Layouts and Navigation

Dynamic routes, nested layouts, Link and loading states.

What you will learn

  • Create dynamic routes
  • Nest layouts
  • Use loading and not-found files

The App Router gives you a small set of special file names. Learn them and you can build almost any navigation structure.

Dynamic routes

Wrap a folder name in square brackets to capture that part of the URL as a parameter.

// app/blog/[slug]/page.tsx  ->  /blog/hello-world
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return <h1>Post: {slug}</h1>;
}

[...slug] catches any number of segments; [[...slug]] also matches the bare path. In recent Next.js versions params is a promise, so await it.

Nested layouts

A layout.tsx in any folder wraps all pages beneath it, and layouts nest. A dashboard can have its own sidebar without affecting the marketing pages.

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ display: "flex" }}>
      <aside>Sidebar</aside>
      <section>{children}</section>
    </div>
  );
}

Route groups

Folders in parentheses organize code without adding to the URL: app/(marketing)/about/page.tsx is still /about. They also let different groups use different layouts.

Special files

  • loading.tsx: shown instantly while a page's data loads (uses React Suspense).
  • error.tsx: shown when something inside throws. It must be a client component.
  • not-found.tsx: shown for a missing page; call notFound() to trigger it.
  • route.ts: an API endpoint instead of a page.
// app/blog/[slug]/loading.tsx
export default function Loading() {
  return <p>Loading post...</p>;
}

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";

export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);      // your data function
  if (!post) notFound();
  return <article><h1>{post.title}</h1></article>;
}
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";

export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
  const active = usePathname() === href;
  return <Link href={href} className={active ? "active" : ""}>{children}</Link>;
}

export function BackButton() {
  const router = useRouter();
  return <button onClick={() => router.back()}>Back</button>;
}
Tip

A folder without a page.tsx is not a route. You can keep components or helpers next to the pages that use them without exposing them as URLs.

Try it yourself

Create /products (a list) and /products/[id] (a detail page that shows the id), plus a loading.tsx for the detail page.

Show solution
// app/products/page.tsx
import Link from "next/link";
export default function Products() {
  return <ul>{[1, 2, 3].map((id) => <li key={id}><Link href={`/products/${id}`}>Product {id}</Link></li>)}</ul>;
}
// app/products/[id]/page.tsx
export default async function Product({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <h1>Product {id}</h1>;
}