Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Zur Übersicht
Kern 15 Min Lesezeit

Server- und Client-Komponenten

Die eine Entscheidung, die du in Next.js ständig triffst

In einem Satz

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.

Bildlich gesprochen

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

entscheidung.txt
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)
app/produkte/page.tsx
// 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>
  );
}
app/produkte/WarenkorbKnopf.tsx
"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>
  );
}
Nur diese kleine Datei landet im Browser. Die Produktliste und der Datenbankzugriff bleiben auf dem Server.

„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.

Wo die Grenze sitzt
✕ die ganze Seite wandert in den Browser
"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>
    </>
  );
}
✓ nur der Knopf wandert
// 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" */}
    </>
  );
}
Tipp Server-Inhalt in Client-Komponenten hineinreichen

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

Nimm es, wenn …
  • +

    Daten holen, Datenbank, Dateisystem, Geheimnisse

  • +

    Große Abhängigkeiten benutzen, ohne dass sie der Nutzer lädt

  • +

    Alles Darstellende, was sich nicht bewegt

Lass es, wenn …
  • −

    useState, useEffect, useRef und alle anderen Hooks

  • −

    onClick und die übrigen Ereignisse

  • −

    window, document, localStorage

  • −

    Context anlegen oder lesen

Stolperstein Die Fehlermeldung, die alle einmal sehen

„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

grenze.tsx
// 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
/>
Gut zu wissen Die Ausnahme: Server Actions

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“.

ablauf.txt
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.

Tipp Absichern lässt es sich zusätzlich

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

⌨ Die Grenze richtig ziehen

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 Korb auf 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>
  );
}

Hirefullstack

Ihr braucht React-Verstärkung im Team?

Wir bauen seit Jahren React- und Next.js-Anwendungen für Kunden in ganz Deutschland – als einzelner Experte, als Verstärkung fürs Bestandsteam oder als komplettes Scrum-Team.

Projekt besprechen →