Zum Inhalt springen
Next.js Fortgeschritten 30 min

Server vs. Client Components

Das zentrale Konzept des App Routers: Server und Client Components, wann du was nutzt und wie du sie korrekt kombinierst.

Aktualisiert:
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/await direkt (Daten holen, DB-Queries)
  • fetch mit 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:

  • async direkt 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:

  1. Server Component = “altes PHP/JSP - rendert HTML mit Daten”
  2. 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-only schuetzt Code vor Client-Einsatz

Im naechsten Kapitel: Data Fetching und Caching im App Router.

Zurück zum Next.js Kurs