React Server Components in Next.js verstehen und anwenden
)
Was sind React Server Components?
React Server Components (RSC) sind eine Komponenten-Art, deren Code ausschließlich auf dem Server ausgeführt wird. Anders als bei klassischen React-Komponenten wird ihr JavaScript nie an den Browser übertragen – es existiert dort schlicht nicht. Das unterscheidet RSC fundamental von serverseitigem Rendering (SSR), wie es React seit Jahren kennt.
Bei klassischem SSR wird der komplette Component-Tree serverseitig zu HTML gerendert, aber der JS-Code jeder einzelnen Komponente wird trotzdem zum Client geschickt und dort hydriert – der Server rendert nur vorab, was der Client ohnehin nochmal tun können muss. Bei RSC entfällt dieser zweite Schritt für Server Components komplett: Sie werden zu einer speziellen Serialisierungsform gerendert, dem sogenannten RSC-Payload, und dieser Payload enthält kein HTML, sondern eine strukturierte Beschreibung des Ergebnisses. Nur die tatsächlich interaktiven Teile – die Client Components – werden weiterhin als JS ausgeliefert und hydriert.
Das Resultat: Server Components tragen mit null Bytes zur Bundle-Größe bei. Egal wie viel Logik, wie viele Abhängigkeiten oder wie viel Datenverarbeitung in einer Server Component steckt – der Browser lädt davon nichts.
Das RSC-Payload-Format
Server Components rendern nicht zu HTML, sondern zu einem eigenen, streambaren Format (in frühen React-Versionen auch "Flight"-Format genannt). Dieses Format enthält:
Serialisierte Elemente-Bäume: eine kompakte Beschreibung der gerenderten Struktur, vergleichbar mit einer sehr effizienten JSON-Repräsentation von React-Elementen
Referenzen auf Client Components als Platzhalter, bestehend aus Modul-ID und Export-Name – der eigentliche Code der Client Component wird also nicht mitgeschickt, sondern nur ein Verweis darauf, welches bereits im Client-Bundle vorhandene Modul an dieser Stelle gerendert werden soll
Streaming-Fähigkeit: Das Payload muss nicht komplett fertig sein, bevor es losgeschickt wird. Komponenten können nach und nach ankommen, während zum Beispiel eine langsame Datenbank-Query im Hintergrund noch läuft
Der Client empfängt dieses Payload, rekonstruiert daraus den virtuellen DOM-Baum und hydriert ausschließlich die referenzierten Client Components. Alles, was als Server Component markiert ist, wird direkt aus der Serialisierung in die Anzeige übernommen – es gibt nichts zu hydrieren, weil es nie interaktiv werden soll.
Der praktische Effekt: Ein Seitenaufbau, der früher entweder komplett synchron warten musste (bis alle Daten da sind) oder komplett client-seitig nachgeladen wurde, kann jetzt granular streamen – Zeile für Zeile, Komponente für Komponente, in der Reihenfolge, in der die Daten verfügbar werden.
Suspense & Streaming im Detail
Die Streaming-Fähigkeit des RSC-Payloads ist kein separates Feature, sondern direkt an <Suspense>-Boundaries gekoppelt. Wenn eine async Server Component innerhalb einer <Suspense>-Grenze auf eine langsame Datenquelle wartet, rendert React zunächst den Fallback dieser Boundary und schickt ihn als Teil des ersten Payload-Chunks. Sobald die Daten verfügbar sind, wird der fertige Inhalt als weiterer Chunk nachgeschickt – inklusive einer kleinen Inline-Script-Anweisung, die den bereits ausgelieferten Fallback im DOM durch den echten Inhalt ersetzt.
export default function Page() {
return (
<div>
<Header /> {/* rendert sofort */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails /> {/* async Server Component, kann warten */}
</Suspense>
</div>
);
}Wichtig dabei: Die Granularität liegt vollständig bei dir. Jede <Suspense>-Boundary erzeugt einen eigenen Streaming-Punkt. Verschachtelte Boundaries erlauben es, dass eine schnelle Komponente sofort erscheint, während eine langsamere daneben noch lädt – ohne dass eine der beiden auf die andere wartet. Fehlt eine explizite Boundary, wartet React beim nächsten übergeordneten <Suspense> (oder im Zweifel bis zum Seitenende), bis auch die langsamste Komponente fertig ist – ein häufiger Grund für unerwartet lange Time-to-First-Byte-Werte, wenn Teams Suspense-Grenzen zu grob setzen.
In Next.js wird dieses Konzept über loading.tsx noch einmal auf Routing-Ebene automatisiert: Next.js umschließt den gesamten Seiteninhalt automatisch mit einer Suspense-Boundary, deren Fallback aus loading.tsx kommt, sobald diese Datei im entsprechenden Routensegment existiert – ohne dass du selbst <Suspense> schreiben musst. Feinere Kontrolle (mehrere unabhängige Ladezustände auf einer Seite) erfordert weiterhin eigene, manuell gesetzte <Suspense>-Boundaries innerhalb der Seite.
RSC ist ein React-Feature, kein Next.js-Feature
Das ist ein Punkt, an dem viele Diskussionen ungenau werden: RSC kommt aus React, nicht aus Next.js. Next.js implementiert und integriert die Technologie – sehr weitgehend sogar – aber das Grundmodell (Server Component als Default, "use client" als Grenzmarkierung, das RSC-Payload-Format) ist Teil von React selbst. Andere Frameworks können RSC ebenso unterstützen, auch wenn Next.js aktuell die mit Abstand ausgereifteste Implementierung ist.
Deshalb lohnt sich die begriffliche Trennung: React Server Components sind das Modell. Next.js Server Components sind dieses Modell plus ein komplettes Anwendungs- und Laufzeitgerüst, das Next.js darum herum gebaut hat. Besonders ab Next.js 16 geht das deutlich über "RSC benutzen" hinaus. Die wichtigsten zusätzlichen Mechanismen, die Next.js oben drauf legt:
App Router – das dateibasierte Routing-System, in dem Server Components der Default sind. Ohne den App Router müsstest du dieses Server/Client-Splitting selbst orchestrieren; Next.js übernimmt das automatisch anhand der Dateistruktur.
Next.js macht Server Components zum Default – React selbst zwingt niemanden zu einem Default. Next.js trifft diese Entscheidung für dich: Jede Datei im App Router ist eine Server Component, bis du explizit "use client" setzt.
Routing und RSC sind miteinander verbunden – Navigation zwischen Routen löst gezielte RSC-Payload-Requests aus, keine vollständigen Seiten-Reloads. Das Routing-System "weiß", welche Teile einer neuen Route bereits im Client vorhanden sind und welche neu geladen werden müssen.
Client Router Cache und Prefetching – Next.js hält bereits geladene RSC-Payloads im Speicher und lädt Ziele von sichtbaren Links im Hintergrund vor, sodass Navigation sich instant anfühlt.
Streaming – React liefert die technische Grundlage (Suspense-Boundaries im RSC-Payload), aber Next.js integriert das in Routing, Layouts und Ladezustände (loading.tsx) zu einem kohärenten Streaming-Modell für ganze Seiten.
Static Rendering vs. Dynamic Rendering – eine Next.js-eigene Entscheidung, ob eine Route zur Build-Zeit oder pro Request gerendert wird. Das ist orthogonal zu RSC selbst, aber eng damit verzahnt, weil sich beide Konzepte gegenseitig beeinflussen.
Cache-System, Cache Components und Partial Prerendering – dieser Bereich hat sich mit Next.js 16 grundlegend verändert. In früheren Versionen cachte Next.js implizit und großzügig; Entwickler mussten sich aktiv aus dem Caching "ausklinken", wenn sie frische Daten brauchten. Mit Cache Components dreht Next.js 16 dieses Verhalten um: Caching ist jetzt vollständig opt-in, sämtlicher dynamischer Code in jeder Page, jedem Layout oder Route Handler wird standardmäßig zur Request-Zeit neu ausgeführt. Caching geschieht explizit über die "use cache"-Direktive, ergänzt um cacheLife für TTLs und cacheTag/updateTag für gezielte Invalidierung. Gleichzeitig lassen sich mit Cache Components auch einzelne UI-Teile als cachefähig markieren, sodass sie Teil des Pre-Render-Durchlaufs neben den statischen Teilen der Seite werden – das ist die technische Fortsetzung von Partial Prerendering, das zuvor ein separates experimentelles Flag war und jetzt fester Bestandteil des Modells ist.
Server Actions / Server Functions – React liefert die "use server"-Direktive als Primitive, Next.js integriert sie tief in Formulare, Revalidierung und den App Router (etwa über useActionState, useFormStatus, useOptimistic).
Revalidation, redirect(), notFound(), error.tsx – Next.js-eigene APIs und Konventionen, um auf Datenänderungen, Weiterleitungen und Fehlerzustände innerhalb des Server-Component-Modells zu reagieren, ohne dass React selbst dafür Vorgaben macht.
Layouts, Route Handlers, Middleware/Proxy – strukturelle Next.js-Konzepte für geteilte UI-Rahmen, eigene API-Endpunkte innerhalb des App Routers und Request-Interception vor dem Rendering. Middleware wurde mit Next.js 16 zugunsten von proxy.ts umbenannt, um die reine Netzwerk-Grenzfunktion klarer vom eigentlichen Rendering abzugrenzen.
Metadata, Image- und Font-Optimization – Next.js-eigene APIs, die zwar nicht direkt Teil des RSC-Modells sind, aber typischerweise innerhalb von Server Components genutzt werden, weil sie serverseitige Verarbeitung voraussetzen (z. B. automatische Bildgrößen-Generierung).
Bundling/Build-System und Environment Variables – Next.js entscheidet, wie das Server/Client-Splitting beim Build tatsächlich umgesetzt wird (inklusive Turbopack als Standard-Bundler seit Next.js 16), und wie Umgebungsvariablen zwischen Server- und Client-Kontext getrennt werden (NEXT_PUBLIC_-Prefix für Client-Zugriff).
Die Konsequenz für dich als Entwickler: Wenn du "mit RSC arbeitest", nutzt du in der Praxis fast immer dieses gesamte Next.js-Gerüst mit – nicht nur das schlanke React-Kernmodell. Das ist wichtig für Architekturentscheidungen, weil ein Wechsel weg von Next.js nicht nur RSC selbst betrifft, sondern auch Routing, Caching, Streaming und ein Dutzend weiterer Mechanismen, die du dann selbst nachbauen müsstest.
Server Components vs. Client Components
Das zentrale Konzept von RSC ist die Aufteilung in zwei Komponenten-Arten mit klar unterschiedlichen Fähigkeiten:
Server Component | Client Component | |
|---|---|---|
Direktive | keine (Default) |
|
Ausführung | Server | Server (initial) + Client (Hydration) |
Bundle-Größe | 0 (bleibt auf Server) | zählt zum Client-Bundle |
Browser-JavaScript | Nein | Ja |
| Nein | Ja |
| Nein | Ja |
Browser-APIs | Nein | Ja |
Datenbankzugriff | Ja | Nein |
Secrets | Ja | Nein |
Event-Handler | Nein | Ja |
Interaktivität | Nein | Ja |
| Ja | normalerweise nicht |
React Context | nicht wie Client-Context | Ja |
Diese Tabelle ist mehr als eine Feature-Liste – sie beschreibt zwei fundamental unterschiedliche Ausführungsumgebungen. Eine Server Component läuft in einer Umgebung mit Zugriff auf Dateisystem, Datenbank-Connections und Secrets, aber ohne jede Vorstellung von "Interaktion", weil sie nie ein zweites Mal ausgeführt wird, nachdem sie einmal gerendert hat. Eine Client Component läuft in genau der Umgebung, die React-Entwickler seit Jahren kennen: mit Zustand, Lebenszyklus und Reaktion auf Nutzereingaben – aber ohne privilegierten Backend-Zugriff.
Der Grund, warum React Context in Server Components "nicht wie Client-Context" funktioniert: createContext und useContext setzen eine Baum-Struktur voraus, die zur Laufzeit im Client existiert und sich über Re-Renders hinweg verändern kann. Server Components rendern aber nur einmal, serverseitig, ohne Re-Render-Zyklus – das Konzept eines "sich ändernden Context-Werts" ergibt dort schlicht keinen Sinn. Ein Context.Provider selbst kann zwar in einer Server Component stehen, aber der eigentliche useContext-Konsum muss in einer Client Component passieren.
Error Handling
Auch Fehlerbehandlung verhält sich auf den beiden Seiten der Grenze unterschiedlich, weil Server- und Client Components zu unterschiedlichen Zeitpunkten und in unterschiedlichen Umgebungen ausgeführt werden.
Fehler in einer Server Component treten während des Renderns auf dem Server auf – zum Beispiel wenn eine Datenbank-Query fehlschlägt oder ein Fetch einen Fehler wirft. Weil Server Components Teil des Streams sind, kann ein solcher Fehler theoretisch mitten im bereits ausgelieferten Payload auftreten, wenn er innerhalb einer <Suspense>-Boundary passiert, die schon zu streamen begonnen hat. React fängt diesen Fall über React Error Boundaries ab, die serverseitig ebenso funktionieren wie client-seitig – die Boundary muss aber selbst eine Client Component sein, weil componentDidCatch/getDerivedStateFromError Klassenkomponenten-Mechanik ist, die nur client-seitig existiert.
// ErrorBoundary.tsx
"use client";
export class ErrorBoundary extends Component<Props, State> {
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) return <FallbackUI />;
return this.props.children; // kann Server-Content als Slot enthalten
}
}In Next.js übernimmt das Framework diese Boundary automatisch über error.tsx: Legst du diese Datei in ein Routensegment, umschließt Next.js den entsprechenden Teil der Seite mit einer Error Boundary, deren Fallback aus dieser Datei kommt. error.tsx ist selbst zwangsläufig eine Client Component (Next.js erzwingt das), weil Error Boundaries – wie oben erklärt – client-seitige Mechanik benötigen.
Fehler in einer Client Component verhalten sich wie klassisches React-Error-Handling: Sie treten während Rendering, in Event-Handlern oder in Effects auf und werden von der nächsten übergeordneten Error Boundary im Client-Baum gefangen. Ein Fehler in einem onClick-Handler wird nicht automatisch von einer Error Boundary gefangen (das war schon vor RSC so) – dafür braucht es weiterhin explizites try/catch im Handler selbst.
Praktischer Unterschied für die Architektur: Weil Server-Component-Fehler potenziell mitten im Streaming auftreten, lohnt es sich, granulare <Suspense>- und Error-Boundary-Paare um einzelne, potenziell fehleranfällige Datenabfragen zu legen (z. B. eine externe API, die gelegentlich timeout-t), statt eine einzige Boundary um die gesamte Seite zu legen. So bricht im Fehlerfall nur der betroffene Seitenausschnitt weg, nicht die komplette Ansicht.
Was "use client" eigentlich macht
Client Components werden mit der Direktive "use client" gekennzeichnet. Wichtig zu verstehen: "use client" ist keine Laufzeit-Direktive, sondern eine Build-Time-Markierung im Modul-Graphen. Sie sagt dem Bundler: "Ab dieser Datei beginnt der Client-Bereich – alles, was von hier aus exportiert und importiert wird, muss ins Client-Bundle." Das ist konzeptionell eher mit einem Compiler-Flag vergleichbar als mit einem React-Hook.
Wichtig: Die Direktive steht am Anfang der Datei, nicht an einzelnen Komponenten. Sie gilt für das gesamte Modul und alles, was daraus exportiert wird.
"use client";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Die Grenze im Modul-Graphen
Stell dir den Modul-Graphen deiner App als Baum vor. Server Components sind der Default-Zustand – die Wurzel. Sobald der Bundler beim Traversieren dieses Baums auf eine Datei mit "use client" stößt, wird ab diesem Punkt der gesamte Unterbaum als Client-Code markiert – unabhängig davon, ob die importierten Module selbst "use client" gesetzt haben oder nicht.
Server Component (page.tsx)
└─ imports ClientLayout.tsx ("use client") ← Boundary!
└─ imports helpers.ts (kein "use client")
└─ dieser Code landet TROTZDEM im Client-Bundle,
weil er transitiv von einer Client-Grenze importiert wirdDas ist ein häufiger Denkfehler: Viele glauben, nur die markierte Datei selbst werde zu Client-Code. Tatsächlich ist es die gesamte Import-Kette ab diesem Punkt. Eine Utility-Datei ohne jede eigene Direktive kann also durchaus im Client-Bundle landen, einfach weil sie von der falschen Stelle aus importiert wird.
Die Einbahnstraßen-Regel: Server → Client ja, Client → Server nein
Diese Regel bestimmt fast alle Composition-Patterns in RSC-Anwendungen:
Server Component importiert Client Component: problemlos, ganz normaler Fall.
Client Component importiert Server Component: funktioniert nicht wie erwartet. Sobald du in einer
"use client"-Datei eine Server Component importierst, wird diese beim Bundling einfach zu einer regulären Client Component "degradiert" – sie verliert ihre Server-Fähigkeiten (kein DB-Zugriff mehr, keinasync function-Component mehr möglich) und ihr Code landet vollständig im Client-Bundle. Das ist meistens nicht das gewünschte Verhalten und einer der klassischsten Einstiegsfehler bei RSC.
Das Slot-Pattern – der Ausweg
Weil man Server Components nicht direkt in Client Components importieren kann, aber trotzdem oft server-gerenderten Content innerhalb interaktiver Client-Wrapper braucht, nutzt man Composition über children/Props:
// ClientTabs.tsx
"use client";
export function ClientTabs({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(0);
return <div>{children}</div>; // children wird nur gerendert, nicht importiert
}
// page.tsx (Server Component)
import { ClientTabs } from "./ClientTabs";
import { ServerHeavyContent } from "./ServerHeavyContent"; // bleibt Server Component!
export default function Page() {
return (
<ClientTabs>
<ServerHeavyContent /> {/* wird VOR dem Rendern von ClientTabs
server-seitig aufgelöst und als
fertiges Element durchgereicht */}
</ClientTabs>
);
}Der Trick: page.tsx (Server Component) rendert ServerHeavyContent server-seitig zu einem fertigen React-Element und übergibt dieses Element als Prop/children an ClientTabs. ClientTabs selbst importiert ServerHeavyContent nie – sie bekommt nur das fertige Ergebnis als Slot gereicht. So bleibt der Server-Code auf dem Server, obwohl er "innerhalb" einer Client Component landet.
Das Gleiche funktioniert mit beliebigen Props, nicht nur children:
<ClientModal trigger={<ServerButton />} content={<ServerContent />} />Prop-Serialisierbarkeit über die Grenze hinweg
Props, die von Server- zu Client Components fließen, müssen serialisierbar sein, weil sie tatsächlich über das RSC-Payload übertragen werden – es handelt sich nicht um normale JavaScript-Referenzen, sondern um Daten, die den Prozess-/Request-Grenzübertritt überstehen müssen.
Erlaubt: Strings, Zahlen, Booleans, Arrays, Plain Objects, Date, React-Elemente, Promises (für use()), Server Actions ("use server"-Funktionen, die speziell serialisiert werden)
Nicht erlaubt: normale Funktionen/Closures, Klassen-Instanzen, Symbols, Map/Set (je nach React-Version), zirkuläre Referenzen
// ❌ Funktioniert nicht
<ClientComponent onSave={() => saveToDb(data)} />
// ✅ Funktioniert – Server Action wird automatisch serialisiert
<ClientComponent onSave={saveAction} /> // saveAction hat "use server"Das wichtigste Architekturprinzip
Eine gute RSC-Anwendung folgt einem einfachen, aber oft missverstandenen Leitsatz:
So viel wie möglich als Server Component bauen – so wenig wie nötig als Client Component.
Nicht die Maxime "alles muss Server Component sein", sondern eine bewusste, kleine Client-Insel innerhalb eines überwiegend server-gerenderten Baums:
Server Component
│
├── Server Component
├── Server Component
│
└── Client Component
├── Client Component
└── Client ComponentSobald eine Komponente "use client" trägt, ist – wie oben beschrieben – ihr gesamter Unterbaum Client-Code. Deshalb sollte diese Grenze so weit unten im Baum wie möglich gezogen werden: nur die tatsächlich interaktiven Blätter werden markiert, nicht ganze Layout-Ebenen.
// ❌ Schlecht: ganze Seite wird Client-Code
"use client";
export default function ProductPage({ product }) {
return (
<div>
<ProductInfo product={product} /> {/* unnötig client-seitig */}
<AddToCartButton /> {/* das ist der einzige interaktive Teil */}
</div>
);
}
// ✅ Besser: nur der Button ist Client Component
export default function ProductPage({ product }) {
return (
<div>
<ProductInfo product={product} /> {/* bleibt Server Component */}
<AddToCartButton /> {/* "use client" nur hier */}
</div>
);
}Der Effekt dieser Granularität ist direkt messbar: Jede Komponente, die auf der Server-Seite der Grenze bleibt, trägt nichts zur Bundle-Größe bei und kann direkt auf Backend-Ressourcen zugreifen. Je konsequenter dieses Prinzip angewendet wird, desto kleiner bleibt der tatsächlich interaktive – und damit client-seitig auszuliefernde – Teil der Anwendung.
Ökosystem-Kompatibilität
Die Granularitäts-Regel klingt in der Theorie einfach, stößt in der Praxis aber schnell auf eine Hürde: nicht jede Library ist RSC-kompatibel. Das betrifft vor allem zwei Kategorien.
Context-basierte State-Manager: Viele ältere State-Management-Lösungen bauen intern auf React.Context auf, um globalen Zustand bereitzustellen. Weil useContext – wie im Kapitel zu Server vs. Client Components erklärt – zwingend client-seitig läuft, kann ein <StoreProvider> niemals eine Server Component sein, selbst wenn er inhaltlich nur statische Konfiguration bereitstellt. Das zieht oft mehr von der App unnötig auf die Client-Seite, als eigentlich nötig wäre.
Ältere UI-Bibliotheken ohne eigene Direktive: Viele Component-Libraries wurden geschrieben, bevor "use client" existierte, und haben deshalb selbst keine Direktive gesetzt – obwohl sie intern Hooks oder Event-Handler nutzen. Importierst du eine solche Komponente direkt in eine Server Component, bricht der Build, weil React dort keine Hooks ausführen kann.
Der gängige Ausweg ist ein dünner Re-Export-Wrapper, der die Direktive für die Library nachträgt:
// components/ThirdPartyButton.tsx
"use client";
export { Button as ThirdPartyButton } from "some-legacy-ui-lib";Diese Wrapper-Datei wird zur eigentlichen Grenze im Modul-Graphen – ab hier ist klar, dass alles, was aus der Library importiert wird, Client-Code ist. Der Vorteil gegenüber einem direkten Import: Du hast einen einzigen, gut sichtbaren Ort, an dem du dokumentierst, welche Third-Party-Komponenten bewusst client-seitig laufen, statt dass diese Entscheidung implizit irgendwo im Codebase verstreut ist.
Testing-Strategie
Server Components lassen sich nicht mit klassischem React-Testing-Tooling (React Testing Library, render(), Event-Simulation) testen, weil sie kein Client-seitiges DOM erzeugen, das man inspizieren könnte – ihr Ergebnis ist das RSC-Payload, nicht ein interaktiver Baum.
Für async Server Components, die im Kern reine Datenaufbereitung sind (Daten laden, transformieren, in JSX gießen), ist der pragmatischste Ansatz, die Komponente wie eine asynchrone Funktion zu behandeln: Sie direkt aufrufen, das zurückgegebene JSX-Element inspizieren (z. B. mit einem Snapshot-Test oder gezielten Assertions auf die Props des Ergebnisbaums), statt sie zu "rendern" im klassischen Sinn.
// ProductInfo.test.ts
import { ProductInfo } from "./ProductInfo";
test("zeigt den Produktnamen", async () => {
const element = await ProductInfo({ productId: "123" });
expect(element.props.children).toContain("Testprodukt");
});Das funktioniert für einfache Fälle, stößt aber an Grenzen, sobald verschachtelte Komponenten und Suspense-Boundaries ins Spiel kommen. Für diese Fälle bleibt aktuell primär End-to-End-Testing (z. B. mit Playwright) die verlässlichste Strategie, weil es die Komponente in einer echten Server-Umgebung rendert und das tatsächliche, im Browser ankommende Ergebnis prüft, statt eine isolierte Rendering-Umgebung zu simulieren, die es für Server Components im eigentlichen Sinn gar nicht gibt.
Client Components dagegen lassen sich weiterhin ganz normal mit React Testing Library testen – sobald die "use client"-Grenze erreicht ist, verhält sich die Komponente wie jede andere React-Komponente auch, inklusive useState, Event-Handlern und Effects. Die Testing-Strategie einer RSC-Anwendung ist also faktisch eine Kombination: Unit-/Snapshot-Tests für die reine Datenlogik in Server Components, klassisches Component-Testing für Client Components, und E2E-Tests für das Zusammenspiel beider – insbesondere für Streaming-Verhalten und Suspense-Fallbacks, die sich isoliert kaum sinnvoll testen lassen.
Fazit
React Server Components sind kein einzelnes Feature, sondern ein komplettes Architektur-Modell mit einer klaren Leitidee: so viel wie möglich auf dem Server halten, so wenig wie nötig an den Client ausliefern. Das RSC-Payload-Format macht das technisch möglich, die "use client"-Grenze im Modul-Graphen macht es explizit – und genau diese Grenze zieht sich durch fast jedes Kapitel dieses Artikels: durch das Slot-Pattern, das sie umgeht, durch die Prop-Serialisierbarkeit, die sie erzwingt, durch die Fehlerbehandlung, die auf beiden Seiten unterschiedlich funktioniert, und durch die Ökosystem-Kompatibilität, an der sie in der Praxis am häufigsten sichtbar wird.
Wichtig ist auch die Trennschärfe zwischen React und Next.js: Das RSC-Modell selbst ist schlank und React-eigen. Was in der Praxis als "RSC-Erfahrung" wahrgenommen wird – Routing, Caching, Streaming auf Seitenebene, Server Actions, Revalidation – ist zu einem großen Teil Next.js-Gerüst obendrauf. Diese Unterscheidung lohnt sich, weil sie klarmacht: Die Entscheidung für RSC ist in der Praxis selten eine rein technische React-Entscheidung, sondern meist eine Framework-Entscheidung mit weitreichenden Konsequenzen.
Wer RSC produktiv einsetzt, kommt an diesem Zusammenspiel nicht vorbei – aber wer die Mechanik einmal verstanden hat (Payload, Grenze, Granularität, Fehlerverhalten), trifft die daraus folgenden Architekturentscheidungen deutlich bewusster, statt sich von Next.js' Defaults treiben zu lassen.