Server- und Client-Komponenten
Die eine Entscheidung, die du in Next.js ständig triffst
Alles läuft auf dem Server, bis du "use client" schreibst. Ab dort läuft es im Browser – und wandert ins JavaScript, das der Nutzer herunterlädt.
Das ist der Kern von Next.js und die Stelle, an der die meisten Fehlermeldungen entstehen. Die gute Nachricht: Es gibt genau eine Frage, die du beantworten musst.
Stell dir eine Zeitung vor. Der größte Teil ist gedruckt – fertig, unveränderlich, billig zu verteilen: das sind Server-KomponenteEine Komponente, die nur auf dem Server läuft. Sie darf direkt Daten holen, aber keine Klicks verarbeiten und keine Hooks benutzen.Im App Router ist das der Normalfall – du musst nichts dafür tun.→. Ein paar Stellen sind aufklappbar oder haben einen Knopf. Nur die brauchen Mechanik, und die muss mitgeliefert werden: das sind Client-KomponenteEine Komponente, die im Browser läuft und deshalb State, Hooks und Klicks kann. Erkennbar an `"use client"` oben in der Datei.→.
Die eine Frage
Braucht dieses Stück …
• useState, useEffect oder einen anderen Hook?
• onClick, onChange, onSubmit?
• window, localStorage, Browser-APIs?
JA → "use client"
NEIN → so lassen (läuft auf dem Server) // Server-Komponente: kein "use client" nötig
import { WarenkorbKnopf } from "./WarenkorbKnopf";
export default async function Seite() {
const produkte = await db.produkt.findMany(); // direkt an die Datenbank
return (
<ul>
{produkte.map((p) => (
<li key={p.id}>
{p.name}
<WarenkorbKnopf id={p.id} /> {/* der Knopf ist interaktiv */}
</li>
))}
</ul>
);
} "use client";
import { useState } from "react";
export function WarenkorbKnopf({ id }: { id: string }) {
const [drin, setDrin] = useState(false);
return (
<button onClick={() => setDrin(true)}>
{drin ? "Im Korb" : "In den Korb"}
</button>
);
} „use client" gilt nach unten weiter
Die Zeile markiert eine Client-GrenzeDie Stelle, an der eine Server-Komponente eine Client-Komponente einbindet. Ab dort wandert alles ins Browser-Bundle.→. Alles, was diese Datei importiert, wird ebenfalls zur Client-Komponente – auch wenn dort kein "use client" steht. Deshalb gilt: so tief wie möglich setzen.
"use client"; // ganz oben in der Seite
export default function Seite() {
const [offen, setOffen] = useState(false);
return (
<>
<RiesigeProduktliste /> {/* jetzt auch Client */}
<button onClick={() => setOffen(true)}>Filter</button>
</>
);
} // Seite bleibt Server-Komponente
export default async function Seite() {
const produkte = await db.produkt.findMany();
return (
<>
<RiesigeProduktliste produkte={produkte} />
<FilterKnopf /> {/* nur diese Datei hat "use client" */}
</>
);
} Eine Client-Komponente darf Server-Komponenten als children enthalten. <ClientTabs><ServerListe /></ClientTabs> funktioniert: Die Liste wird auf dem Server erzeugt und nur als fertiges Ergebnis durchgereicht. Wichtig ist nur, dass die Client-Komponente sie nicht selbst importiert.
Was Server-Komponenten nicht können
- +
Daten holen, Datenbank, Dateisystem, Geheimnisse
- +
Große Abhängigkeiten benutzen, ohne dass sie der Nutzer lädt
- +
Alles Darstellende, was sich nicht bewegt
- −
useState,useEffect,useRefund alle anderen Hooks - −
onClickund die übrigen Ereignisse - −
window,document,localStorage - −
Context anlegen oder lesen
„You're importing a component that needs useState. It only works in a Client Component, but none of its parents are marked with use client.“ Übersetzt: Du benutzt einen Hook in einer Datei, die auf dem Server läuft. Entweder "use client" an den Anfang – oder den interaktiven Teil in eine eigene Datei ziehen.
Props über die Grenze
// Von Server an Client dürfen nur Dinge, die sich verschicken lassen:
<ClientTeil
titel="Hallo" // ✅ Text
anzahl={3} // ✅ Zahl
daten={{ a: 1 }} // ✅ einfaches Objekt
datum={new Date()} // ✅ geht auch
beiKlick={() => …} // ❌ Funktionen nicht
db={prismaClient} // ❌ Klasseninstanzen nicht
/> Eine Funktion darfst du doch übergeben, wenn sie mit "use server" markiert ist. Dann schickt Next.js nicht die Funktion, sondern einen Verweis darauf. Genau darauf bauen Formulare auf – dazu später eine eigene Lektion.
▸ Genauer erklärt: Wie kommt das Ergebnis in den Browser? optional
Der Server schickt kein fertiges HTML für die Server-Komponenten, sondern eine kompakte Beschreibung – den RSC-PayloadDas Datenformat, in dem der Server das Ergebnis seiner Komponenten an den Browser schickt – kein HTML, sondern eine Beschreibung.→. Darin steht: „hier Text, dort ein Loch, in das die Client-Komponente Foo gehört, mit diesen Props“.
1. Server führt die Server-Komponenten aus
2. Ergebnis wird als RSC-Payload verschickt
(+ beim ersten Aufruf zusätzlich fertiges HTML)
3. Browser lädt NUR das JavaScript der Client-Komponenten
4. React setzt beides zusammen und macht die Knöpfe lebendig Deshalb ist der Code deiner Server-Komponenten nie im Browser zu sehen – auch nicht in den Entwicklerwerkzeugen. Ein API-Schlüssel dort ist tatsächlich sicher.
Das Paket server-onlyEin kleines Paket, das den Build abbricht, falls eine Datei versehentlich im Browser landet. Schutz für Code mit Geheimnissen.→ bricht den Build ab, falls eine Datei mit Geheimnissen doch einmal von einer Client-Komponente importiert wird. Ein Import, eine Sorge weniger.
Sitzt das schon?
5 Fragen zu dieser Lektion. Falsche Antworten landen in deiner Statistik.
Jetzt selbst schreiben
Hier ist der interaktive Teil einer Produktkarte – der Knopf, der etwas in den Warenkorb legt. Baue ihn als Client-Komponente fertig. (Im Editor läuft reines React; in Next.js käme oben zusätzlich "use client" in die Datei.)
- □Vor dem Klick steht
In den Korbauf dem Knopf - □Nach dem Klick steht
Im Korb (1) - □Ein weiterer Klick erhöht auf
Im Korb (2) - □Die Anzahl steht in einem Element mit
data-testid="anzahl"
import { useState } from "react"; // In Next.js stünde hier oben: "use client"; export function WarenkorbKnopf() { // TODO: Anzahl merken und Beschriftung danach richten return <button data-testid="anzahl">???</button>; } export default function App() { return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <h3>Espressomaschine</h3> <WarenkorbKnopf /> </div> ); }