webdev.complete
šŸ—‚ļø Next.js App Router
šŸš€Next.js & T3
Lesson 91 of 117
30 min

Routing & Layouts

page, layout, template, loading, error, not-found.

In Next.js, the file system isthe router. You don't register routes in a config. You don't write a route table. You make a folder called about, drop a page.tsxinside, and now /aboutworks. That's the entire idea of the App Router, and once it clicks, you stop thinking about routing at all.

The app/ directory

Every route lives under app/. A folder name becomes a URL segment. To turn a folder into an actual page, you add a special file with a reserved name. The most important ones are page.tsx, layout.tsx, loading.tsx,error.tsx, not-found.tsx, and template.tsx. Anything else is just a regular module and doesn't get routed.

Folder tree to URL

bash
app/
ā”œā”€ā”€ layout.tsx          // wraps every page
ā”œā”€ā”€ page.tsx            // /
ā”œā”€ā”€ about/
│   └── page.tsx        // /about
ā”œā”€ā”€ blog/
│   ā”œā”€ā”€ layout.tsx      // wraps /blog/*
│   ā”œā”€ā”€ page.tsx        // /blog
│   └── [slug]/
│       └── page.tsx    // /blog/anything
└── dashboard/
    ā”œā”€ā”€ layout.tsx      // wraps /dashboard/*
    ā”œā”€ā”€ page.tsx        // /dashboard
    └── settings/
        └── page.tsx    // /dashboard/settings
One page per route
A folder without a page.tsx is not routable. You can still have nested folders for organization (layouts, route groups, private folders prefixed with _) and they won't create URLs.

page.tsx: the leaf

A page.tsxis the component that renders at that URL. By default it's a React Server Component, which means it runs on the server, can be async, and never ships to the browser as JavaScript.

app/about/page.tsx
export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>We make widgets.</p>
    </main>
  );
}

Want async data? Just add async. No getServerSideProps, no getStaticProps, no special hooks. The function literally awaits.

app/users/page.tsx
async function getUsers() {
  const res = await fetch("https://api.example.com/users");
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers();
  return (
    <ul>
      {users.map((u) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

layout.tsx: persistent wrappers

A layout wraps every page below it. Crucially, layouts do not re-renderwhen you navigate between sibling pages. That's the killer feature. Your sidebar state, scroll position, video player, and any client component state inside the layout stays alive as the user moves around.

app/layout.tsx
// The root layout is required. It must include <html> and <body>.
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

Layouts nest. A dashboard/layout.tsx wraps every page under /dashboard/*, and the root layout still wraps all of that. Same composition rules as React, just spelled with files instead of imports.

When to lift state into a layout
If two sibling pages share a sidebar that has open/closed state you want to preserve across navigation, put it in the layout. Putting it in each page would reset it on every route change.

template.tsx: the opposite of layout

A template.tsx looks identical to a layout but with one critical difference: it remountson every navigation. New DOM nodes, new state, new effects. You almost never need this, but it's the right tool when you want enter/exit animations or want effects to run on every route change.

app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
  // useEffect inside any client component below will re-run on every navigation
  return <div className="page-enter">{children}</div>;
}

loading.tsx: instant Suspense

Drop a loading.tsx next to a page and Next automatically wraps that route in a React Suspense boundary, using your file as the fallback. When the page does async work, the loading UI shows immediately while the server streams the real content.

app/blog/loading.tsx
export default function Loading() {
  return <p>Loading posts...</p>;
}

You can also use <Suspense> manually inside any component for finer-grained boundaries. loading.tsxis a convenience for the "whole route" case.

error.tsx: route-scoped error boundaries

When something throws inside a route segment, the nearest error.tsx renders instead. It receives the error and a reset function to try again.

app/dashboard/error.tsx
"use client"; // error files must be client components

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something broke</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
error.tsx must be a client component
Error boundaries are a React feature that runs in the browser. The"use client" directive is required at the top.

not-found.tsx: 404 with style

Render this when a page calls notFound() or no route matches.

app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div>
      <h2>404</h2>
      <p>That page doesn&apos;t exist.</p>
      <Link href="/">Home</Link>
    </div>
  );
}

Trigger it from anywhere in a Server Component:

tsx
import { notFound } from "next/navigation";

const post = await getPost(slug);
if (!post) notFound();

How they nest, visualized

For a request to /dashboard/settings, Next composes the tree top down. Imagine each layer wrapping the next:

bash
<RootLayout>
  <DashboardLayout>
    <ErrorBoundary fallback={<DashboardError />}>
      <Suspense fallback={<DashboardLoading />}>
        <SettingsPage />
      </Suspense>
    </ErrorBoundary>
  </DashboardLayout>
</RootLayout>

Quiz

Quiz1 / 3

What's the difference between layout.tsx and template.tsx?

Recap

  • Folder = URL segment. page.tsx makes a folder routable.
  • layout.tsx wraps every page below it and persists across navigation.
  • template.tsx wraps like a layout but remounts on every navigation.
  • loading.tsx is an automatic Suspense boundary;error.tsx is an automatic error boundary (must be a client component).
  • not-found.tsx renders for 404s and calls to notFound().