8. Next.js I - Struktur und Routing
Next.js I: Struktur und Routing
Abschnitt betitelt „Next.js I: Struktur und Routing“React rendert Komponenten, mehr nicht. Für eine echte Anwendung fehlt einiges: mehrere Seiten mit URLs, ein Server, der sie ausliefert, ein Build-Prozess, der TypeScript und JSX übersetzt, und später ein Ort für die eigene API. All das könnte man einzeln zusammenstecken; ein Framework liefert es fertig verdrahtet. Der Lehrplan verlangt die Entwicklung komplexer Anwendungssysteme “unter Verwendung von Frameworks und Middleware”, und unser Framework ist Next.js: der verbreitetste Rahmen um React, der Client und Server in einem Projekt vereint.
Die Begriffe sauber getrennt: Eine Bibliothek (React) wird von unserem Code aufgerufen; ein Framework (Next.js) ruft unseren Code auf und gibt die Struktur vor, in die er sich einfügt. Diese Umkehrung der Kontrolle ist das Wesen jedes Frameworks, und sie erklärt, warum ein Framework Konventionen hat, die man lernt statt diskutiert.
Projekt anlegen
Abschnitt betitelt „Projekt anlegen“-
Projekt erzeugen:
Terminal-Fenster npx create-next-app@latest render-farmDie Fragen des Assistenten: TypeScript ja, ESLint ja, App Router ja, der Rest nach Geschmack (Tailwind wird in den Beispielen nicht vorausgesetzt).
-
Entwicklungsserver starten:
Terminal-Fenster cd render-farmnpm run devDie Anwendung läuft auf
http://localhost:3000und lädt bei jeder Dateiänderung automatisch neu (Hot Reload). -
Struktur ansehen. Die wichtigsten Bestandteile:
render-farm/├── app/ # pages, layouts, later API routes│ ├── layout.tsx # root layout (html, body, navigation)│ └── page.tsx # the start page /├── public/ # static files (images, favicon)├── package.json # scripts and dependencies├── next.config.ts # framework configuration└── tsconfig.json # TypeScript, preconfigured strict
Bemerkenswert ist, was fehlt: kein Webpack-Setup, keine tsc-Aufrufe, kein Server-Skript. Das Framework übernimmt Build, TypeScript und Serverbetrieb; npm run dev für die Entwicklung, npm run build und npm start für den Produktionsbetrieb.
App Router: Ordner sind Routen
Abschnitt betitelt „App Router: Ordner sind Routen“Next.js leitet die URLs der Anwendung aus der Ordnerstruktur unter app/ ab. Ein Ordner ist ein URL-Segment, die Datei page.tsx darin macht das Segment zur aufrufbaren Seite:
app/├── page.tsx -> /├── jobs/│ ├── page.tsx -> /jobs│ └── [id]/│ └── page.tsx -> /jobs/42, /jobs/7, ...└── presets/ └── page.tsx -> /presetsEine Seite ist eine gewöhnliche React-Komponente als Default-Export:
export default function JobsPage() { return <h1>Render jobs</h1>;}Dynamische Segmente stehen in eckigen Klammern: Der Ordner [id] fängt jeden Wert an dieser Stelle, und die Seite erhält ihn über die params-Prop (in aktuellen Next-Versionen als Promise, daher await):
export default async function JobDetailPage({ params,}: { params: Promise<{ id: string }>;}) { const { id } = await params; return <h1>Job {id}</h1>;}Damit ist das URL-Schema aus dem API-Entwurfskapitel direkt abbildbar: Sammlung unter /jobs, Element unter /jobs/42. Für unbekannte Pfade liefert Next automatisch einen 404; eine eigene Datei not-found.tsx gestaltet ihn.
Layouts: der gemeinsame Rahmen
Abschnitt betitelt „Layouts: der gemeinsame Rahmen“Eine Datei layout.tsx umschließt alle Seiten ihres Ordners und aller Unterordner. Das Wurzel-Layout trägt das HTML-Gerüst und die Navigation, die auf jeder Seite erscheint:
import Link from "next/link";
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="de"> <body> <nav> <Link href="/">Start</Link> <Link href="/jobs">Jobs</Link> <Link href="/presets">Presets</Link> </nav> <main>{children}</main> </body> </html> );}children ist die jeweils aktive Seite; das Layout selbst bleibt beim Seitenwechsel erhalten (samt seinem Zustand, etwa einem geöffneten Menü). Layouts verschachteln sich: Ein zusätzliches app/jobs/layout.tsx umschließt nur die Jobs-Seiten, innerhalb des Wurzel-Layouts. Navigiert wird mit der Link-Komponente statt <a>: Sie wechselt die Seite ohne komplettes Neuladen und lädt Ziele im Hintergrund vor. Dieses Wurzel-Layout mit Navigation ist ab jetzt das gemeinsame Gerüst, in dem alle weiteren Übungen dieses Jahres wohnen.
Server- und Client-Komponenten
Abschnitt betitelt „Server- und Client-Komponenten“Jetzt zur Frage, die den Rest des Jahres begleitet: Wo läuft mein Code? Next.js führt Komponenten an zwei Orten aus, und der Unterschied ist keine Nebensache, sondern das Architekturmerkmal des Frameworks. Als Einstieg ordnet das folgende Video die beiden Komponentenarten und ihre Aufgaben ein:
Server-Komponenten sind der Standard: Jede Komponente ohne besondere Markierung wird auf dem Server gerendert, und nur das fertige HTML erreicht den Browser. Server-Komponenten dürfen deshalb Dinge, die im Browser undenkbar wären: direkt auf Dateien und (später) Datenbanken zugreifen, mit Geheimnissen wie API-Schlüsseln arbeiten, und sie dürfen async sein:
// app/presets/page.tsx: a server component reading a fileimport { readFile } from "node:fs/promises";
export default async function PresetsPage() { const raw = await readFile("data/presets.json", "utf-8"); const presets: string[] = JSON.parse(raw); return ( <ul> {presets.map((preset) => <li key={preset}>{preset}</li>)} </ul> );}Genau dieses Vorgehen heißt Server-Side Rendering (SSR): Der Server erzeugt die Daten, rendert daraus fertiges HTML und schickt es an den Browser, der es sofort anzeigt und anschließend interaktiv macht (“hydriert”). Der folgende Ablauf zeigt die vier Schritte von der Anfrage bis zur fertigen, interaktiven Seite:
Client-Komponenten laufen im Browser und beginnen mit der Direktive "use client" in der ersten Zeile. Nur sie können, was Interaktivität braucht: Hooks (useState, useEffect, …), Event-Handler (onClick, onChange), Browser-APIs (localStorage).
// app/jobs/job-filter.tsx: interactivity needs the client"use client";
import { useState } from "react";
export function JobFilter({ names }: { names: string[] }) { const [query, setQuery] = useState(""); const visible = names.filter((n) => n.includes(query)); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul>{visible.map((n) => <li key={n}>{n}</li>)}</ul> </> );}Das Gegenstück zu SSR ist Client-Side Rendering (CSR): Der Server liefert nur ein minimales HTML-Gerüst und das JavaScript-Bundle; erst im Browser rendert die durch "use client" markierte Komponente den tatsächlichen Inhalt. Der Ablauf verschiebt die Renderarbeit damit auf die Client-Seite:
Die Entscheidungstabelle:
| Bedarf | Server | Client |
|---|---|---|
| Daten laden (Datei, Datenbank, interne API) | ja | über HTTP |
| Geheimnisse (Schlüssel, Zugangsdaten) | ja | niemals |
| Hooks, Events, Interaktivität | nein | ja |
Browser-APIs (localStorage, …) | nein | ja |
Das Standardmuster kombiniert beide: Eine Server-Komponente beschafft die Daten und übergibt sie als Props an eine Client-Komponente, die die Interaktivität übernimmt (genau so arbeiten PresetsPage und JobFilter zusammen). Zwei Regeln dazu: "use client" wirkt ansteckend (alles, was eine Client-Komponente importiert, wird Teil des Client-Bundles), deshalb sitzt die Direktive so tief wie möglich im Komponentenbaum, nicht am Layout. Und die Grenze ist eine Sicherheitsgrenze: Was eine Client-Komponente als Prop erhält, steht im Browser und damit jedem Besucher offen; Geheimnisse bleiben auf der Server-Seite.
Wie dieses Zusammenspiel konkret aussieht und wo die Grenze zwischen Server und Client im Code verläuft, führt das folgende Video vor:
Statisch oder dynamisch: wann der Server rendert
Abschnitt betitelt „Statisch oder dynamisch: wann der Server rendert“SSR sagt, wo gerendert wird (auf dem Server). Es bleibt aber eine zweite Frage: wann? Beim npm run build entscheidet Next.js das für jede Seite selbst, und es gibt zwei Möglichkeiten:
- Statisch (static): Die Seite wird einmal zur Bauzeit gerendert, das fertige HTML wird gespeichert und an jeden Besucher unverändert ausgeliefert. Sehr schnell, aber der Inhalt ist ab dem Build eingefroren. Das ist der Standardfall, immer wenn eine Seite von nichts Veränderlichem abzuhängen scheint.
- Dynamisch (dynamic): Die Seite wird bei jeder Anfrage neu auf dem Server gerendert. Etwas langsamer, dafür immer aktuell.
Im Build-Bericht stehen beide markiert, ○ für statisch, ƒ für dynamisch:
Route (app)┌ ○ / (statisch, zur Bauzeit gerendert)├ ƒ /jobs (dynamisch, pro Anfrage gerendert)└ ○ /about (statisch)Meist trifft Next.js die richtige Wahl von allein: Sobald eine Seite etwas erkennbar Veränderliches nutzt (Daten aus einer Datenbank, das Lesen von headers() oder cookies(), ein fetch ohne Zwischenspeicher), schaltet sie automatisch auf dynamisch. Kniffelig wird es nur, wenn eine Seite veränderliche Daten liest, Next.js das aber nicht erkennt - etwa eine gewöhnliche Modul-Variable, die sich zur Laufzeit ändert. Dann baut Next.js die Seite fälschlich statisch und friert sie auf den Zustand zur Bauzeit ein. Für genau diesen Fall gibt es einen ausdrücklichen Schalter, den man aus der Datei exportiert:
export const dynamic = "force-dynamic"; // immer pro Anfrage rendern, nie einfrierenexport const dynamic = "force-dynamic" ist keine gewöhnliche Programmlogik, sondern eine Deklaration an das Framework: eine von mehreren Konstanten (dynamic, revalidate, runtime, …), die eine page.tsx oder route.ts exportieren kann und die Next.js beim Bauen ausliest. In den meisten Anwendungen braucht man sie nie, weil ein echter Datenzugriff die Seite ohnehin dynamisch macht. Man greift dazu, wenn eine Seite sichtbar veraltet, obwohl sich die Daten geändert haben - ein sicheres Zeichen, dass sie versehentlich statisch gebaut wurde.
Hydration: vom HTML zur interaktiven Seite
Abschnitt betitelt „Hydration: vom HTML zur interaktiven Seite“Bei SSR erreicht den Browser zuerst fertiges HTML. Es ist sofort sichtbar und lesbar, aber noch tot: Ein Klick auf einen Button bewirkt nichts, ein onChange feuert nicht, useState hält noch keinen Zustand. Der Grund ist einfach: Das HTML ist nur die Beschreibung der Oberfläche; die Ereignis-Handler und der Zustand leben im JavaScript, das erst noch geladen und ausgeführt werden muss.
Diesen zweiten Schritt nennt man Hydration (Hydrieren, wörtlich “mit Wasser versorgen”): React lädt im Browser das Client-Bundle, geht das bereits vorhandene HTML durch, hängt an jedes Element die passenden Event-Handler und stellt den Komponentenzustand her. Erst danach ist die Seite interaktiv. Wichtig ist, dass React das HTML dabei nicht neu aufbaut, sondern das vom Server gelieferte übernimmt und nur “belebt” — genau deshalb ist SSR schnell sichtbar und am Ende voll interaktiv.
Daraus folgen zwei Dinge, die in der Praxis regelmäßig auffallen:
- Die Reihenfolge ist sichtbar. Zwischen “HTML da” und “hydriert” liegt eine kurze Spanne. In dieser Zeit sieht der Benutzer die Seite schon, ein Klick geht aber noch ins Leere. Bei kleinen Seiten ist das kaum wahrnehmbar, bei großen Client-Bundles kann es spürbar werden — ein Grund, das Client-Bundle klein zu halten.
- Server- und Client-Ausgabe müssen übereinstimmen. React erwartet, dass das im Browser erzeugte HTML exakt zu dem passt, was der Server geschickt hat. Wer beim ersten Rendern etwas verwendet, das auf Server und Client verschieden ist —
Date.now(),Math.random(),localStorage,window—, erhält einen Hydration-Mismatch: React meldet in der Konsole, dass sich Server- und Client-HTML unterscheiden. Solche Werte gehören deshalb nicht in das erste Rendern, sondern in einenuseEffect, der erst nach der Hydration im Browser läuft.
Wie weit "use client" reicht
Abschnitt betitelt „Wie weit "use client" reicht“"use client" markiert nicht eine einzelne Komponente, sondern eine Grenze im Komponentenbaum. Die Direktive steht ganz oben in einer Datei und erklärt: Ab hier — in dieser Komponente und in allem, was sie importiert und rendert — sind wir auf der Client-Seite. Deshalb heißt es, "use client" sei “ansteckend”: Der ganze Teilbaum unterhalb der Grenze wird Teil des Client-Bundles, auch Komponenten, die selbst gar kein "use client" tragen.
Betrachten wir einen Baum mit einem interaktiven Zweig:
RootLayout (Server)└── DashboardPage (Server) ├── StatsPanel (Server) └── JobFilter "use client" ← Grenze ├── SearchBox (im Client-Bundle) └── ResultList (im Client-Bundle) └── ResultRow (im Client-Bundle)JobFilter setzt die Direktive. Damit landen SearchBox, ResultList und ResultRow automatisch im Client-Bundle — nicht, weil sie selbst markiert wären, sondern weil sie unterhalb der Grenze liegen. StatsPanel dagegen ist ein Geschwister von JobFilter, kein Kind, und bleibt eine reine Server-Komponente. RootLayout und DashboardPage bleiben ebenfalls auf dem Server.
Zwei Regeln machen den Umgang damit beherrschbar:
-
Die Grenze so tief wie möglich setzen. Je weiter oben
"use client"steht, desto größer der Teilbaum, der in den Browser wandert. Steht die Direktive versehentlich im Wurzel-Layout, wird die gesamte Anwendung zur Client-Anwendung, und der Vorteil der Server-Komponenten ist verloren. Deshalb sitzt"use client"am kleinsten wirklich interaktiven Baustein. -
Server-Inhalte über
childrendurch die Grenze reichen. Angenommen, wir wollen einen aufklappbaren Rahmen (Accordion, interaktiv, also Client) um eine Statistik (ServerStats, liest Daten vom Server). Entscheidend ist der Unterschied zwischen importieren und übergeben.Der naheliegende, aber falsche Weg: Die Client-Komponente importiert die Server-Komponente direkt.
accordion.tsx "use client";import { useState } from "react";import { ServerStats } from "./server-stats"; // imported directlyexport function Accordion() {const [open, setOpen] = useState(false);return (<div><button onClick={() => setOpen(!open)}>Details</button>{open && <ServerStats />} {/* pulls ServerStats into the client bundle */}</div>);}Durch den
importliegtServerStatsunterhalb der Client-Grenze und wird Teil des Client-Bundles — der Server-Vorteil ist weg (und Datei-/Datenbankzugriff inServerStatswürde jetzt gar nicht mehr funktionieren).Der richtige Weg:
AccordionimportiertServerStatsnicht, sondern nimmt beliebigen Inhalt alschildrenentgegen. Zusammengesteckt werden beide erst eine Ebene höher, in einer Server-Komponente.accordion.tsx "use client";import { useState } from "react";export function Accordion({ children }: { children: React.ReactNode }) {const [open, setOpen] = useState(false);return (<div><button onClick={() => setOpen(!open)}>Details</button>{open && children} {/* renders whatever it is given, without knowing it */}</div>);}// page.tsx (server component)export default function DashboardPage() {return (<Accordion><ServerStats /> {/* rendered here, in a server context */}</Accordion>);}Weil
<ServerStats />inpage.tsx(Server) steht und nicht inaccordion.tsx(Client), rendert der Server es fertig und übergibt nur das Ergebnis alschildren.Accordionsteuert lediglich, ob dieser fertige Inhalt angezeigt wird — nicht, wie er entsteht. So bleibtServerStatseine echte Server-Komponente, obwohl sie optisch in einem interaktiven Client-Rahmen sitzt.
Lernergebnisse: Was Sie nach diesem Kapitel können sollten
Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:
- Erklären: den Unterschied zwischen Bibliothek und Framework erklären und benennen, welche Aufgaben Next.js gegenüber purem React übernimmt.
- Anwenden: ein Next.js-Projekt anlegen, den Entwicklungsserver nutzen und die Projektstruktur erklären.
- Anwenden: Seiten über die Ordnerstruktur anlegen, dynamische Segmente (
[id]) auslesen und mitLinknavigieren. - Anwenden: gemeinsame Rahmen als (verschachtelte) Layouts umsetzen.
- Erklären: Server- und Client-Komponenten unterscheiden, die Entscheidungsregeln anwenden und begründen, warum Geheimnisse nie den Client erreichen dürfen.
- Erklären: statisches und dynamisches Rendern unterscheiden und erkennen, wann eine Seite
export const dynamic = "force-dynamic"braucht. - Entwerfen: Seiten so in Server- und Client-Anteile zerlegen, dass Interaktivität und Datenbeschaffung am jeweils richtigen Ort liegen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 14 - Mehrseiter mit Next.js