EMZETT.
Login

Next.js App Router

Kurz: Next.js’ Routing- und Rendering-System (seit Version 13), bei dem Seiten standardmäßig serverseitig gerendert werden (“Server Components”) und nur explizit markierte Teile im Browser laufen (“Client Components”, "use client").

Genauer: next/dynamic mit ssr:false (eine Komponente komplett vom Server-Rendering ausschließen) funktioniert nur innerhalb einer Client Component — in einer Server Component (wie z. B. layout.tsx ohne "use client") ist das ein harter Build-Fehler, kein Warning. npx tsc --noEmit prüft nur TypeScript-Typen und fängt solche Next.js-spezifischen Regeln nicht ab — dafür braucht es einen echten next build.

Kontext bei uns: Ein ssr:false-Fehler genau dieser Art hat einmal den Build kaputt gemacht — seitdem läuft vor jedem größeren Batch zusätzlich ein echter npm run build, nicht nur der TypeScript-Check.

Im Detail

Dateibasiertes Routing über spezielle Dateinamen

Der App Router organisiert Routen über die Ordnerstruktur unter app/ — jeder Ordner entspricht einem URL-Segment, eine page.tsx-Datei darin macht den Ordner zu einer echten aufrufbaren Seite (Ordner ohne page.tsx sind nur strukturell, z. B. für gemeinsam genutzte layout.tsx-Dateien). Das ist ein fundamentaler Unterschied zum älteren Pages Router (pages/-Verzeichnis), wo jede Datei automatisch eine Route war und Layouts manuell in jede Seite importiert werden mussten. Neben page.tsx gibt es eine ganze Reihe weiterer reservierter Dateinamen mit fester Bedeutung: layout.tsx umschließt alle darunterliegenden Seiten und bleibt bei Navigation innerhalb desselben Layouts bestehen (kein Re-Mount), loading.tsx wird automatisch als Suspense-Fallback gezeigt während eine Server Component lädt, error.tsx fängt Laufzeitfehler in seinem Teilbaum ab (muss selbst eine Client Component sein), und not-found.tsx wird gerendert, wenn notFound() aufgerufen wird. Ordner in eckigen Klammern ([slug]) erzeugen dynamische Routensegmente, doppelte eckige Klammern ([...slug]) fangen beliebig viele Pfadsegmente auf einmal ein (“Catch-all”-Routen).

Server Components als Standardfall

Server Components sind der Standardfall: Sie laufen ausschließlich auf dem Server, haben direkten Zugriff auf Datenbanken/Secrets, senden aber selbst KEINEN JavaScript-Code an den Browser — das Ergebnis ist bereits fertiges HTML. Client Components ("use client" am Dateianfang) werden dagegen zusätzlich als JavaScript an den Browser geschickt und dort “hydriert”, damit sie interaktiv werden (onClick, useState, useEffect funktionieren nur dort). Die Faustregel: so viel wie möglich als Server Component belassen, nur dort "use client" setzen, wo echte Interaktivität im Browser nötig ist — jede Client Component vergrößert das an den Browser gesendete JavaScript-Bundle. Wichtig dabei: Die "use client"-Direktive markiert nicht nur eine einzelne Komponente, sondern die gesamte Modul-Grenze — jede Komponente, die von einer Client Component importiert wird, wird ebenfalls Teil des Client-Bundles, selbst wenn sie selbst kein useState verwendet.

// app/posts/[slug]/page.tsx — Server Component (Standard, kein "use client")
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await db.query.posts.findFirst({ where: eq(posts.slug, params.slug) });
  return <article>{post?.title}</article>;
}

Ein verbreitetes Muster ist es, Server und Client Components zu kombinieren, indem man eine kleine, interaktive Client Component als children-Prop in eine Server Component einbettet — die Server Component bleibt dabei Server-only, obwohl sie eine Client Component “enthält”:

// app/layout.tsx — Server Component, rendert eine Client Component als Kind
import { ThemeToggle } from "./theme-toggle"; // Client Component
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeToggle />
        {children}
      </body>
    </html>
  );
}

Datenladen und Caching

Server Components können direkt async/await verwenden, um Daten zu laden — kein useEffect und kein separater Loading-State im Client nötig, da das Warten serverseitig passiert, bevor überhaupt HTML an den Browser geht. Next.js erweitert die native fetch-API dabei um ein eigenes Caching-Verhalten: Ergebnisse werden standardmäßig zwischengespeichert und über Builds hinweg wiederverwendet, sofern nicht explizit cache: "no-store" oder eine revalidate-Option gesetzt wird. Das ist eine häufige Fehlerquelle für Einsteiger, die aus dem klassischen React ohne Server-Rendering kommen und ein implizites “bei jedem Request neu laden” erwarten.

Typische Fallstricke

Ein ssr:false-Fehler bei next/dynamic (siehe oben) ist nur einer von mehreren Next.js-spezifischen Regeln, die tsc nicht erkennt — auch das Vermischen von Server- und Client-only-APIs (z. B. window in einer Server Component) fällt erst bei next build bzw. zur Laufzeit auf. Ein weiterer klassischer Fehler: Props von einer Server Component an eine Client Component müssen serialisierbar sein (keine Funktionen, keine Klassen-Instanzen), da sie effektiv über die Server/Client-Grenze “transportiert” werden müssen.

Siehe auch: next-intl, Cache-Tags und revalidateTag