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 →