Start / Cheat Sheets

Cheat Sheet

Next.js 16 Kern — Cheat Sheet

Stand: · Next.js 16 Full-Stack-Entwicklung mit React und TypeScript

Next.jsApp RouterServer ComponentsCache Components

Der Spickzettel für alles, was eine Seite auf den Schirm bringt: Routing, Layouts, die Server-Client-Grenze, Streaming und Caching. Was danach kommt — Formulare, Datenbank, Anmeldung, Tests, Betrieb — steht auf dem zweiten Blatt: Next.js 16 Full-Stack.

Maßgeblich ist und bleibt die offizielle Next.js-Dokumentation; dieses Blatt ist eine Auswahl für den Alltag, keine Referenz. Es beschreibt Next.js 16.3 mit React 19.2 — ältere Anleitungen im Netz weichen an genau den Stellen ab, die hier stehen.

Projekt und Konventionsdateien

npx create-next-app@latest    # TypeScript und App Router wählen

Die Antworten beim Anlegen wirken lange nach: Der Ordner src/ und der Pfad-Alias @ lassen sich später nur mit einer Sammelmigration nachrüsten. Turbopack ist in 16.3 Standard, keine Option — alte webpack-Konfiguration greift nicht mehr. Der Build-Cache liegt lokal und hilft auf frischen CI-Runnern zunächst nicht.

DateiBedeutung
page.tsxmacht das Verzeichnis zur Route
layout.tsxHülle, bleibt bei Navigation bestehen
template.tsxHülle, wird bei Navigation neu erzeugt
loading.tsxLadezustand der Route
error.tsxFehlergrenze der Route
not-found.tsxAntwort auf notFound
route.tsEndpunkt statt Seite
proxy.tsläuft vor der Anfrage, im Wurzelverzeichnis neben app/

Alles ohne diese Namen bleibt gewöhnliche Datei — auch mitten in app/. Komponenten und Tests dürfen also neben ihrer Route liegen, ohne eine URL zu erzeugen; ein Ordner mit Unterstrich (_komponenten/) bleibt vollständig aus dem Routing heraus.

Modul: Einordnung und Projektsetup

Routing: Segmente, Gruppen, Slots

app/page.tsx                     /
app/liegeplaetze/[nummer]/page.tsx   /liegeplaetze/42
app/(portal)/buchungen/page.tsx      /buchungen   ← Klammern fallen weg
app/dashboard/@belegung/page.tsx     benannter Slot, kein eigener Pfad
SchreibweiseBedeutung
[nummer]genau ein Segment
[...pfad]ein oder mehrere Segmente, kommt als Array
[[...pfad]]null oder mehr Segmente
(gruppe)Struktur ohne URL-Wirkung, eigenes Layout möglich
_ordnervollständig aus dem Routing heraus

Parameter kommen als Promise und sind Benutzereingabe:

export default async function Page(props: PageProps<'/liegeplaetze/[nummer]'>) {
  const { nummer } = await props.params   // PageProps wird beim Build erzeugt
}

Modul: App Router und Routing-Grundlagen

Layout oder Template

LayoutTemplate
bei Navigationbleibt bestehenneu erzeugt
Zustanderhaltenzurückgesetzt
Effektelaufen einmallaufen erneut
NormalfalljaAusnahme

Im Zweifel Layout. Template nur, wenn der Zustand ausdrücklich weg soll — ein mehrstufiger Assistent etwa, der beim zweiten Aufruf keine alten Eingaben zeigen darf. Das Wurzel-Layout ist verpflichtend und muss <html> und <body> enthalten.

Parallele Routen (@slot) und abfangende Routen sind seltener richtig, als sie wirken:

FallMittel
Zwei Bereiche, eigene Ladezuständeparallele Route
Detail als Overlay, URL soll teilbar seinabfangende Route
Zwei Bereiche, ein gemeinsamer Ladezustandgewöhnliche Komponente
Inhalt hängt an einem SchalterClient-Zustand

Modul: App Router und Routing-Grundlagen

LinkuseRouter
ohne JavaScriptjanein
Prefetchingjanein
aus Code auslösbarneinja
Client-Komponente nötigneinja

Partial Prefetching setzt Cache Components voraus, beide Schalter gehören zusammen:

// next.config.ts
const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true }
EinstellungUmfang
Standarddie App-Hülle der Route
prefetch true am Linkzusätzlich URL-abhängige Inhalte, kostet einen Serveraufruf je Link
prefetch falsenichts

Prefetching ist im Entwicklungsmodus abgeschaltet — Navigationsprobleme fallen dort nicht auf.

Modul: Navigation, Prefetching und UI-Zustände

Lade-, Fehler- und Statuszustände

MittelZuständig für
loading.tsxLadezustand einer ganzen Route
<Suspense>Ladezustand eines Teilbaums, näher am Zugriff
error.tsxFehler einer ganzen Route
global-error.tsxFehler, die das Wurzel-Layout treffen
catchErrorFehler eines einzelnen Teilbaums, mit retry

Je enger die Grenze, desto mehr Seite bleibt beim Fehler stehen. Für die Antwort an den Aufrufer gibt es vier Aufrufe, und ihre Reihenfolge ist nicht beliebig — erst Identität, dann Recht, dann Existenz:

LageAufrufStatus
nicht angemeldetunauthorized()401
angemeldet, kein Rechtforbidden()403
Datensatz existiert nichtnotFound()404
Ziel hat sich verschobenredirect()307

Alle vier werfen eine Kontrollfluss-Ausnahme: Code dahinter läuft nicht mehr, und ein try-Block, der alles abfängt, verschluckt sie.

Modul: Navigation, Prefetching und UI-Zustände

Server oder Client Component

Server Component (Standard)Client Component (begründete Ausnahme)
Datenbank und API abfragenZustand und Ereignisse
Geheimnisse verwendenuseEffect und Lebenszyklus
JavaScript im Bundle sparenBrowser-APIs wie localStorage
früh sichtbaren Inhalt lieferneigene Hooks

'use client' steht ganz oben in der Datei, über allen Importen, und wirkt auf den Modulgraphen: Alles, was diese Datei importiert, landet im Bundle. Die Direktive gehört deshalb an den Filterknopf, nicht an die Seite.

Der Ausweg aus fast jedem vermeintlichen Konflikt ist das Slot-Muster — importieren wandert ins Bundle, durchreichen nicht:

<Aufklapper titel="Saison">      {/* Client-Hülle */}
  <Saisonstatistik />            {/* bleibt Server Component */}
</Aufklapper>

Was die Grenze passiert, ist im Browser im Klartext lesbar — auch ausgeblendete Felder. Props schmal schneiden statt ganze Datensätze durchreichen:

<GastKarte name={gast.name} boot={gast.bootsname} />   // nicht: gast={gast}
import 'server-only'   // ein Import in eine Client-Datei bricht den Build ab
Form der VariablenSichtbarkeit
DATABASE_URL, SESSION_SECRETnur Server
NEXT_PUBLIC_…im Bundle, öffentlich

Das Präfix ist keine Einstellung, sondern eine Veröffentlichung — und sie bleibt in bereits ausgelieferten Bundles bestehen.

Modul: Server und Client Components

Daten laden und streamen

const plaetzeP = getLiegeplaetze()          // starten …
const rechnungenP = getOffeneRechnungen()
const [plaetze, rechnungen] = await Promise.all([plaetzeP, rechnungenP])

Die Aufrufe starten beim Aufruf, nicht beim await — deshalb erst sammeln.

LageForm
Abfragen unabhängigparallel starten, gemeinsam erwarten
zweite braucht Ergebnis der erstensequenziell, zweite in Suspense
ein Fehler darf nicht alles kippenallSettled statt all
Segmente der Routelaufen ohnehin parallel

cache() aus React verhindert Doppelarbeit innerhalb eines Requests — der saubere Weg, die Sitzung überall zu lesen, ohne sie durchzureichen:

export const getSitzung = cache(async () => { /* … */ })
FallWo geladen wird
beim Rendern bekanntServer Component
nach Interaktion nachgeladenClient mit Bibliothek
regelmäßig aktualisiertClient mit Bibliothek
einmal geladen, dann gefiltertServer laden, Client filtern

Die letzte Zeile ist der Fall, den man am häufigsten falsch löst. Wer aus dem Client lesen muss: Das Versprechen auf dem Server starten, nicht erwarten, durchreichen und im Client mit use() auflösen.

Laufzeit-APIQuelle
cookies()Cookies der Anfrage
headers()Kopfzeilen der Anfrage
searchParamsAbfrageteil der URL
paramsdynamische Pfadsegmente

Früher zog ein einziger cookies()-Aufruf die ganze Route ins dynamische Rendern. Heute begrenzt die Suspense-Grenze den Schaden auf ihren Teilbaum — ältere Dokumentation und Beiträge beschreiben an dieser Stelle noch das alte Verhalten. Ein Zugriff in generateMetadata zählt genauso wie einer in der Seite.

Modul: Datenzugriff und Streaming

Caching mit Cache Components

Wer nichts markiert, bekommt frische Daten — das ist der sichere Ausgangszustand. Gecacht wird ausdrücklich:

export async function getBelegung() {
  'use cache'
  cacheLife('minutes')
  cacheTag('belegung')      // der Griff, an dem die Server Action später zieht
  return db.select().from(belegungen)
}

Argumente gehen in den Schlüssel ein — und alles, was die Funktion aus ihrem Umfeld liest, ebenfalls.

DatenLebensdauerGrund
HafenbeschreibungTagefast statisch
TariflisteStundenändert sich selten
BelegungMinutenändert sich laufend
Personenbezogenesgar nichtsiehe unten
SpeicherortGeteiltDauerhaft
Standard, im Prozessneinnein — serverlos oft flüchtig
use cache remotejaja, kostet einen Netzwerkweg je Zugriff
use cache privateneinim Browser des Nutzers
vorgerendertes HTMLjaja

Ein neuer Build macht alle Einträge ungültig, auch die dauerhaften.

AufrufWirkung
revalidateTagmarkiert Einträge als veraltet
updateTagerneuert die Einträge sofort
revalidatePathdasselbe auf Pfadebene
refreshaktualisiert den Client-Router, nicht getaggte Daten

Partial Prerendering setzt das zusammen: statische Hülle vom CDN, gecachte Teile daneben, personalisierte Teile strömen nach.

import { connection } from 'next/server'
await connection()        // nötig vor Date.now(), Math.random(), randomUUID()

Modul: Caching mit Cache Components

Typische Fallen

  • Ein Ordner ohne page ist keine Route und liefert 404, auch mit layout. Die Wurzel-Route heißt app/page.tsx, nicht app/index.tsx.
  • params ist ein Promise. Ein await darauf ganz oben macht alles Folgende sequenziell; im Layout verhindert es das Vorrendern des ganzen Zweigs.
  • Ein Layout rendert bei Navigation im eigenen Zweig nicht erneut. Eine Prüfung allein im Layout läuft deshalb nicht bei jedem Seitenwechsel — und ausgeblendete Inhalte stehen trotzdem in der übertragenen Nutzlast.
  • Route Groups sind keine Sicherheitsgrenze, nur eine Strukturhilfe. Zwei Gruppen dürfen nicht dieselbe URL erzeugen, sonst bricht der Build ab.
  • fetch wird nicht mehr von allein gecacht und blockiert bis zur Antwort.
  • Ein Layout, das ungecachte Daten liest, fällt nicht auf sein eigenes loading zurück — es blockiert die Navigation. Sitzungsdaten im Kopfbereich sind der häufigste Auslöser; ein await im Wurzel-Layout trifft jede Seite.
  • Suspense allein macht nichts dynamisch. Synchrone Arbeit läuft trotzdem vorab; Bots bekommen die fertig gerenderte Seite, nicht den Strom.
  • Eine gecachte Abfrage, die die Sitzung liest, liefert fremde Daten aus. Der Fehler zeigt sich erst bei zwei gleichzeitigen Nutzern. Wer die Kennung als Argument übergibt, macht sie zum Cache-Schlüssel — das ist der saubere Weg.
  • Ein Tag ohne passende Invalidierung ist wirkungslos, und über mehrere Instanzen hinweg wirkt er nur mit koordinierendem Cache-Handler.
  • Beim Selbsthosten muss der Reverse Proxy die Pufferung abschalten, sonst kommt vom Streaming nichts an.

Zum Seminar Next.js 16 Full-Stack-Entwicklung mit React und TypeScript