Server Component und Client Component in Next.js
1. Übersicht
Der Next.js App Router rendert Komponenten standardmäßig auf dem Server, ganz anders als das “alles läuft auf dem Client”-Modell des früheren Pages Router. Dieser Artikel hilft dir zu verstehen, wie sich Server Components und Client Components unterscheiden, wann du "use client" zwingend nutzen musst, und wie du beide Arten in einem Render-Baum kombinierst, ohne auf Fehler zu stoßen. Nach dem Lesen wirst du selbst entscheiden können, ob eine Komponente auf dem Server oder Client sein sollte, und du wirst wissen, wie du Daten über diese Grenze hinweg korrekt weitergibst.
2. Was ist eine Server Component?
Eine Server Component ist die Standard-Art von Komponente im App Router (Verzeichnis app/). Sie läuft vollständig auf dem Server und sendet kein eigenes JavaScript an den Browser.
// app/products/page.tsx
async function getProducts() {
const res = await fetch("https://api.shop.local/products", { cache: "no-store" });
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p: { id: number; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Ein paar wichtige Punkte:
ProductsPageist eineasync function— Server Components dürfen direktawaitverwenden, ohneuseEffectoderuseStatefür das Datenabrufen zu brauchen.- Der
fetch-Befehl läuft auf dem Server, API-Schlüssel und sensible Logik sind immer vor dem Browser verborgen. - Es gibt keine
"use client"-Zeile am Anfang der Datei, also versteht Next.js automatisch, dass dies eine Server Component ist.
2.1. Warum sollten Server Components bevorzugt werden?
Server Components reduzieren das JavaScript-Bundle, das an den Client gesendet wird, weil die Komponente selbst (und ihre Imports, sofern sie nicht klientifiziert werden) nicht in das Browser-Bundle gepackt werden. Sie können auch direkt auf Datenbanken, das Dateisystem oder geheime Umgebungsvariablen zugreifen, ohne diese über eine API-Route offenzulegen.
Request → Server render (fetch, DB query) → HTML + RSC payload → Browser
Der Browser erhält nur bereits gerendertes HTML zusammen mit einer Payload, die den Komponentenbaum beschreibt (RSC-Payload), aber nicht den JS-Quellcode der Server Component.
3. Was ist eine Client Component?
Eine Client Component ist eine Komponente, die im Browser laufen muss — um State, Effects, Event Handler oder Web-APIs wie localStorage oder window zu nutzen. Du deklarierst sie mit der Direktive "use client" am Anfang der Datei.
// app/components/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "❤️ Mag das" : "🤍 Mag"}
</button>
);
}
Erklärung:
"use client"muss die erste Zeile der Datei sein (vor allen Imports), sie markiert eine Grenze — alles, was von dieser Datei importiert wird, wird auch ins Client-Bundle gepackt.useStatefunktioniert nur in Client Components, weil JavaScript im Browser laufen muss, um Re-Renders zu verwalten.onClickist ein Event Handler — ein sicheres Zeichen, dass du eine Client Component brauchst.
Hinweis:
"use client"bedeutet nicht, dass die Komponente nur auf dem Client läuft. Next.js rendert sie immer noch zunächst auf dem Server (SSR), um schnell HTML zu haben, und “hydratisiert” sie dann im Browser, um sie interaktiv zu machen.
4. Wann musst du Client Component verwenden?
Es gibt mehrere Fälle, in denen du "use client" hinzufügen musst, sonst triffst du auf Build- oder Runtime-Fehler:
Error: useState can not be used in Server Components.
Diese Fälle sind:
- Verwendung von React Hooks:
useState,useEffect,useReducer,useContext… - Anfügen von Event Handlern:
onClick,onChange,onSubmit… - Verwendung von Web-APIs, die nur im Browser existieren:
window,document,localStorage. - Verwendung von Custom Hooks, die die oben genannten Dinge enthalten.
- Verwendung von Bibliotheken, die nur auf dem Client laufen (z. B. einige Charting-Bibliotheken, die auf
windowbasieren).
Umgekehrt: Wenn eine Komponente nur Props empfängt und reines JSX rendert, keine Hooks, keine Events — lass sie einfach eine Server Component sein, auch wenn sie tief in einem Baum mit anderen Client Components verschachtelt ist.
5. Server und Client Components kombinieren
Das Beste an diesem Modell ist, dass du Server Components über children (Composition) in Client Components verschachteln kannst, statt sie direkt zu importieren.
// app/components/Modal.tsx
"use client";
import { useState } from "react";
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Details öffnen</button>
{open && <div className="modal">{children}</div>}
</>
);
}
// app/orders/[id]/page.tsx
import Modal from "@/app/components/Modal";
async function getOrder(id: string) {
const res = await fetch(`https://api.shop.local/orders/${id}`);
return res.json();
}
export default async function OrderPage({ params }: { params: { id: string } }) {
const order = await getOrder(params.id);
return (
<Modal>
{/* OrderDetail ist immer noch eine Server Component, wird nur als "children" weitergeleitet */}
<p>Bestellnummer: {order.id}</p>
<p>Gesamtbetrag: {order.total.toLocaleString("de-DE")} €</p>
</Modal>
);
}
Erklärung des Mechanismus:
Modalist eine Client Component, die den State für Öffnen/Schließen verwaltet.- Der Inhalt darin (
<p>Bestellnummer...) wird auf derOrderPagegerenderetiert — einer Server Component — und dann alschildrenanModalweitergeleitet. - Next.js rendert diese
childrenzunächst auf dem Server und “steckt” dann das Ergebnis bei der Hydratisierung in die Position{children}der Client Component. Dadurch muss die Datenbeschaffung nicht klientifiziert werden.
Hinweis: Du kannst nicht direkt eine
asyncServer Component importieren und dann in einer Client Component verwenden (z. B.import OrderDetail from "./OrderDetail"in einer Datei mit"use client"). Next.js wird die Komponente als ins Client-Bundle gezogen betrachten und einen Fehler auslösen, daasync function componentauf dem Client nicht gültig ist. Composition überchildrenoderpropsist der richtige Weg.
6. Daten von Server Component zu Client Component weitergeben
Props, die von Server Component zu Client Component weitergeleitet werden, müssen serialisierbar sein — das heißt, sie müssen in JSON konvertierbar sein. Du kannst keine Funktionen, Klasseninstanzen oder komplexe Date-Objekte weitergeben.
// app/components/PriceTag.tsx
"use client";
export default function PriceTag({ amount, currency }: { amount: number; currency: string }) {
return <span>{amount.toLocaleString("de-DE")} {currency}</span>;
}
// app/products/[id]/page.tsx
import PriceTag from "@/app/components/PriceTag";
export default async function ProductDetailPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.shop.local/products/${params.id}`).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<PriceTag amount={product.price} currency="EUR" />
</div>
);
}
Hier sind amount (Zahl) und currency (String) primitive Daten, die serialisierbar sind. Wenn du versuchst, einen Callback von der Server Component zu PriceTag weiterzugeben, wird Next.js einen Fehler bei Build/Runtime auslösen:
Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".
Wenn du Logik auf dem Server von der Client-Seite aus aufrufen möchtest (z. B. Form-Submit), verwendest du eine Server Action — markiert mit "use server" — anstatt eine normale Funktion weiterzugeben.
7. Server Action — die Brücke zurück
Wenn eine Client Component den Server aufrufen muss (Daten schreiben, DB aufrufen), verwende eine Server Action statt eine eigene API-Route zu erstellen.
// app/actions/order-actions.ts
"use server";
export async function cancelOrder(orderId: string) {
await fetch(`https://api.shop.local/orders/${orderId}/cancel`, { method: "POST" });
}
// app/components/CancelButton.tsx
"use client";
import { cancelOrder } from "@/app/actions/order-actions";
import { useState } from "react";
export default function CancelButton({ orderId }: { orderId: string }) {
const [pending, setPending] = useState(false);
async function handleClick() {
setPending(true);
await cancelOrder(orderId);
setPending(false);
}
return (
<button onClick={handleClick} disabled={pending}>
{pending ? "Wird storniert..." : "Bestellung stornieren"}
</button>
);
}
cancelOrderist mit"use server"markiert, Next.js generiert automatisch einen versteckten Endpoint, den die Client Component aufrufen kann, ohne dass du einen Route Handler schreiben musst.CancelButtonimportiert und ruft ihn wie eine normale Funktion auf — der Code liest sich natürlich, obwohl es tatsächlich ein Netzwerkaufruf ist.
8. Tests für getrennte Logik schreiben
Da Server Components meist Logik zum Abrufen/Transformieren von Daten enthalten, ist es einfacher, die reine Logik (ohne JSX) zu trennen und zu testen, als die ganze Komponente zu rendern.
// app/lib/pricing.ts
export function formatPrice(amount: number, currency: string): string {
if (amount < 0) {
throw new Error("amount darf nicht negativ sein");
}
return `${amount.toLocaleString("de-DE")} ${currency}`;
}
// app/lib/pricing.test.ts
import { describe, it, expect } from "vitest";
import { formatPrice } from "./pricing";
describe("formatPrice", () => {
it("formatiert gültige Geldbeträge", () => {
expect(formatPrice(150000, "EUR")).toBe("150.000 EUR");
});
it("wirft Fehler bei negativem amount", () => {
expect(() => formatPrice(-1, "EUR")).toThrow("amount darf nicht negativ sein");
});
});
Wenn dein Projekt Jest + Testing Library für Client Components (z. B. LikeButton) nutzt, testest du sie wie normale React-Komponenten — Jest muss nichts über "use client" wissen, da diese Direktive nur für den Next.js-Bundler Bedeutung hat und nicht die Runtime-Tests beeinflusst.
// app/components/LikeButton.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import LikeButton from "./LikeButton";
describe("LikeButton", () => {
it("ändert Status beim Klick", () => {
render(<LikeButton />);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("Mag");
fireEvent.click(button);
expect(button).toHaveTextContent("Mag das");
});
});
9. Häufige Fehler
Ein paar häufig auftretende Fehler beim Wechsel zum App Router:
"use client"beim Verwenden von Hooks vergessen: FehleruseState can not be used in Server Components— füge die Direktive am Anfang der Datei, die den Hook enthält, ein, nicht in der Datei, die ihn aufruft."use client"zu hoch im Komponentenbaum platzieren: Wenn du sogarlayout.tsxals Client markierst, wird alles darin (einschließlich untergeordneter Server Components) ins Client-Bundle gezogen, wenn sie direkt importiert werden, was das Bundle unnötig aufbläht. Klientifiziere nur die kleinsten notwendigen Komponenten (z. B. Schaltflächen, Formulareingaben), nicht die ganze Seite.- Funktionen oder nicht serialisierbare Objekte über Props von Server zu Client Component weitergeben — verwende stattdessen eine Server Action (
"use server"). - Einen Server Component in einer Datei mit
"use client"importieren und erwarten, dass er immer noch auf dem Server läuft — das geht nicht, du musst Composition überchildren/propsverwenden. fetchmit geheimen Schlüsseln in einer Client Component aufrufen — der API-Schlüssel wird im JS-Bundle an den Browser gesendet. Operationen mit Geheimnissen müssen immer in einer Server Component oder Server Action sein.
Hinweis: Es ist nicht sicher, einfach “alles in Client Components umzuwandeln, damit es wie alter Code aussieht”. Jede unnötige Client Component erhöht die Bundle-Größe, verlangsamt die Interaktionszeit (TTI) und verliert die Vorteile des Server-Renderings.
10. Fazit
Server Components sind der Standard im App Router und eignen sich zum Abrufen von Daten, zum Zugriff auf sensible Ressourcen und zur Reduzierung der JS-Bundle. Client Components solltest du nur verwenden, wenn du State, Effects oder Event Handler hast. Diese beiden Arten kombinieren sich durch Composition (children, serialisierbare props) und Server Actions sind die Brücke, damit Client Components serverseitige Logik aufrufen können. Das Verständnis dieser Grenzen hilft dir, die Bundle-Größe zu optimieren und sensible Daten auf dem Server zu schützen.