Server vs. Client Components
Das zentrale Konzept des App Routers: Server und Client Components, wann du was nutzt und wie du sie korrekt kombinierst.
Inhaltsverzeichnis
Server vs. Client Components
Das neue Mental Model von Next.js: Komponenten rendern entweder auf dem Server oder im Browser. Das veraendert, wie du React schreibst.
Das Default
Im App Router ist alles standardmaessig eine Server Component:
// app/page.tsx - das ist eine Server Component
export default async function Page() {
const posts = await db.posts.findMany();
return (
<main>
{posts.map(post => <article key={post.id}>{post.title}</article>)}
</main>
);
}
Das laeuft auf dem Server. Der Browser bekommt das fertige HTML, minimal JS (nur fuer Hydration), und keine Daten-Fetching-Waterfalls.
Client Component markieren
Wenn du Interaktivitaet brauchst - State, Events, Browser-APIs - markierst du die Komponente als Client:
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Geklickt: {count}
</button>
);
}
Das "use client" oben bedeutet: “Diese Komponente und alles, was sie importiert, laeuft im Browser.”
Was geht wo?
Server Components koennen:
async/awaitdirekt (Daten holen, DB-Queries)fetchmit eingebautem Caching- Geheime Daten (Env-Variablen, Secrets) - landen nicht im Browser
- Datenbank-Zugriff (Prisma, Drizzle, SQL)
- Server-only Libraries nutzen (fs, node:crypto, etc.)
Server Components koennen NICHT:
- State haben (
useState,useReducer) - Effects (
useEffect) - Event-Handler (
onClick,onChange) - Browser-APIs (
window,localStorage) - React-Kontext bereitstellen/konsumieren
Client Components koennen:
- Alles, was klassisches React kann
- State, Effects, Events
- Browser-APIs
- React Context
Client Components koennen NICHT:
asyncdirekt als Default-Export sein (Workarounds existieren)- Auf Server-only-Secrets zugreifen
Die einfache Regel
Server Components by default. Client nur wenn noetig.
Das Muster, das in praxis funktioniert:
- Pages, Layouts, Daten-Display: Server
- Formulare, Counter, Modals, Tabs, alles mit State/Events: Client
- Server wrappt Client so tief wie moeglich im Baum
Das Muster: Server rendert Client
Du kannst Server Components um Client Components bauen:
// app/page.tsx (Server Component)
import { Counter } from "@/components/Counter";
export default async function Page() {
const daten = await fetchData(); // laeuft auf Server
return (
<main>
<h1>{daten.titel}</h1>
<p>{daten.text}</p>
<Counter /> {/* Interaktive Insel */}
</main>
);
}
// components/Counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
So bleibt dein Bundle klein - der fetchData-Call landet nicht im Browser, nur der Counter.
Das Anti-Pattern: Alles zum Client machen
"use client";
// ...alles hier drin, inklusive Daten-Fetching via useEffect
Folgen:
- Extra Render-Zyklus (Loading → Daten da)
- Groesseres JS-Bundle
- Schlechteres SEO
- API-Routes noetig (weil DB-Zugriff nicht im Browser geht)
Vermeide das, wo immer moeglich.
Props von Server zu Client
Server Components koennen Daten an Client Components uebergeben - aber die Daten muessen serialisierbar sein:
// Server
export default async function Page() {
const user = await getUser(); // { name: "Anna", alter: 28 }
return <Profile user={user} />;
}
// Client
"use client";
export function Profile({ user }: { user: User }) {
const [editing, setEditing] = useState(false);
return <div>{user.name}</div>;
}
Was geht NICHT als Prop
- Funktionen (ausser Server Actions - dazu spaeter)
- Klassen-Instanzen (z.B. Date-Objekte - die werden zu Strings)
- Maps, Sets (bei Props zu Client - serialisierbar machen)
Children als Escape Hatch
Eine Client-Komponente kann children von Server-Komponenten rendern:
// Client Component
"use client";
export function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Oeffnen</button>
{open && <dialog>{children}</dialog>}
</>
);
}
// Server Component
import { Modal } from "./Modal";
export default async function Page() {
const daten = await fetchData();
return (
<Modal>
<h2>{daten.titel}</h2>
<p>{daten.text}</p>
</Modal>
);
}
Der Modal-State ist im Browser, der Content wird auf dem Server gerendert - genial.
Context nutzen
Context geht nur in Client Components. Fuer einen Provider, der ueberall verfuegbar sein soll:
// components/providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
// app/layout.tsx
import { Providers } from "@/components/providers";
export default function RootLayout({ children }: Props) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Das Pattern: Dünner Client-Wrapper am Root, alles andere bleibt Server.
Haeufige Bibliotheken
Viele Libraries muessen Client sein:
- Framer Motion - animations
- React-Hook-Form - forms
- React-Query / SWR - client-side caching
- Chart-Bibliotheken - DOM-Zugriff
Du wrappst sie in Client Components:
"use client";
export { motion } from "framer-motion";
Oder einfach: Die Komponente, die sie nutzt, ist "use client".
Server-Only Libraries
Manche Packages sollen nicht im Browser landen (Secrets, grosse Deps):
import "server-only";
export async function getSecretData() {
// ...
}
Wenn du diese Datei versehentlich in eine Client Component importierst → Build-Fehler. Super zum Schutz von Secrets.
Dynamische Imports fuer Client-Only-Code
Manchmal willst du eine Komponente nur im Browser laden (z.B. Chart-Libraries):
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./Chart"), { ssr: false });
export default function Page() {
return <Chart />;
}
ssr: false verhindert Server-Rendering - hilft bei Libraries, die kein SSR vertragen.
Debug: Warum laeuft das auf dem Client?
Next.js zeigt in der Dev-Console, wenn eine Server Component etwas importiert, das Client-only ist. Die Fehlermeldung ist meist aussagekraeftig.
Das Mental Model
Stell dir vor:
- Server Component = “altes PHP/JSP - rendert HTML mit Daten”
- Client Component = “klassisches React mit State/Events”
Der Trick: Du kannst sie mischen. Der Server rendert das statische Geruest plus die Initial-Daten, und im richtigen Moment “uebernehmen” Client Components die Interaktion.
Praktisches Beispiel
Eine Blog-Seite mit Like-Button:
// app/post/[slug]/page.tsx (Server)
import { LikeButton } from "@/components/LikeButton";
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await db.posts.findBySlug(slug);
return (
<article>
<h1>{post.titel}</h1>
<p>{post.datum}</p>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
);
}
// components/LikeButton.tsx (Client)
"use client";
import { useState } from "react";
export function LikeButton({
postId,
initialLikes,
}: {
postId: number;
initialLikes: number;
}) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(false);
async function toggle() {
setLiked(!liked);
setLikes(liked ? likes - 1 : likes + 1);
await fetch(`/api/posts/${postId}/like`, { method: "POST" });
}
return (
<button onClick={toggle} aria-pressed={liked}>
{liked ? "❤️" : "🤍"} {likes}
</button>
);
}
Der Artikel ist Server-gerendert (SEO, schnell), nur der Like-Button ist interaktiv.
Zusammenfassung
- Server Components sind Default - schreibe sie zuerst
"use client"nur wo State/Events/Browser-APIs noetig- Server wrappt Client - nicht umgekehrt
children-Pattern fuer Mix-Komponenten- Props muessen serialisierbar sein
server-onlyschuetzt Code vor Client-Einsatz
Im naechsten Kapitel: Data Fetching und Caching im App Router.