React Server Components: Das neue Mental Model für 2026
Umfassendes Verständnis von React Server Components. Composition Patterns, Data Fetching, Client/Server Boundary und Migration von Client-only Apps.

React Server Components: Das neue Mental Model für 2026
Meta-Description: Umfassendes Verständnis von React Server Components. Composition Patterns, Data Fetching, Client/Server Boundary und Migration von Client-only Apps.
Keywords: React Server Components, RSC, Server Components, React 19, Next.js, Client Components, Hybrid Rendering
Einführung
React Server Components (RSC) sind nicht nur ein Feature – sie sind ein neues Mental Model. Server Components rendern auf dem Server, senden kein JavaScript zum Client und können direkt auf Backend-Ressourcen zugreifen.
Das Mental Model
┌─────────────────────────────────────────────────────────────┐
│ REACT SERVER COMPONENTS MODEL │
├─────────────────────────────────────────────────────────────┤
│ │
│ SERVER CLIENT │
│ ────────────────────── ────────────────────── │
│ ┌─────────────────────┐ │
│ │ Server Component │ ┌─────────────────────┐ │
│ │ ├── DB Access │ →→→ │ HTML + RSC Payload │ │
│ │ ├── File System │ │ (No JS Bundle!) │ │
│ │ ├── API Calls │ └─────────────────────┘ │
│ │ └── Heavy Compute │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Client Component │ →→→ │ JS Bundle │ │
│ │ ├── useState │ │ (Minimal!) │ │
│ │ ├── useEffect │ └─────────────────────┘ │
│ │ ├── Event Handlers │ │
│ │ └── Browser APIs │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Server Components vs. Client Components
| Aspekt | Server Component | Client Component |
|---|---|---|
| **Rendering** | Server | Server + Client |
| **JavaScript** | 0 bytes zum Client | Wird gebundelt |
| **State** | Nein (kein useState) | Ja |
| **Effects** | Nein (kein useEffect) | Ja |
| **Event Handlers** | Nein | Ja |
| **Browser APIs** | Nein | Ja |
| **DB/Filesystem** | Ja (direkt) | Nein |
| **async/await** | Ja (top-level) | Nein |
Composition Patterns
Pattern 1: Server Component als Wrapper
// app/products/page.tsx (Server Component)
import { ProductGrid } from '@/components/ProductGrid';
import { FilterPanel } from '@/components/FilterPanel';
async function ProductsPage() {
// Server-side data fetching
const products = await prisma.product.findMany({
include: { category: true }
});
return (
<div className="flex">
{/* Client Component für Interaktivität */}
<FilterPanel categories={products.map(p => p.category)} />
{/* Server Component rendert Produkte */}
<ProductGrid products={products} />
</div>
);
}Pattern 2: Client Component Children
// components/Modal.tsx (Client Component)
'use client';
import { useState } from 'react';
export function Modal({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Öffnen</button>
{isOpen && (
<div className="modal">
{/* Children können Server Components sein! */}
{children}
<button onClick={() => setIsOpen(false)}>Schließen</button>
</div>
)}
</>
);
}
// Verwendung (Server Component)
async function ProductPage({ params }) {
const product = await fetchProduct(params.id);
return (
<div>
<Modal>
{/* Server Component als Child von Client Component */}
<ProductDetails product={product} />
</Modal>
</div>
);
}Pattern 3: Props Serialization
// ❌ Fehler: Funktionen können nicht serialisiert werden
async function ProductCard({ product }) {
return (
<div>
{product.name}
{/* Fehler: onClick kann nicht zum Client */}
<button onClick={() => console.log('clicked')}>
Click
</button>
</div>
);
}
// ✅ Richtig: Interaktivität in Client Component
async function ProductCard({ product }) {
return (
<div>
{product.name}
{/* Client Component für Click Handler */}
<ProductActions productId={product.id} />
</div>
);
}
// components/ProductActions.tsx
'use client';
export function ProductActions({ productId }: { productId: string }) {
return (
<button onClick={() => addToCart(productId)}>
In den Warenkorb
</button>
);
}Data Fetching Patterns
Pattern 1: Parallel Data Fetching
// ❌ Sequential (langsam)
async function Dashboard() {
const user = await fetchUser();
const stats = await fetchStats(); // Wartet auf user
const notifications = await fetchNotifications(); // Wartet auf stats
return <DashboardView {...{ user, stats, notifications }} />;
}
// ✅ Parallel (schnell)
async function Dashboard() {
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications()
]);
return <DashboardView {...{ user, stats, notifications }} />;
}Pattern 2: Data Colocation
// Jede Component holt ihre eigenen Daten
// (Next.js dedupliziert automatisch gleiche Requests)
async function UserProfile() {
const user = await fetchUser(); // Request 1
return <Profile user={user} />;
}
async function UserPosts() {
const user = await fetchUser(); // Dedupliziert!
const posts = await fetchPosts(user.id);
return <PostList posts={posts} />;
}
async function UserPage() {
return (
<div>
<UserProfile />
<UserPosts />
</div>
);
}Pattern 3: Streaming mit Suspense
import { Suspense } from 'react';
async function ProductPage({ params }) {
return (
<div>
{/* Sofort gerendert */}
<ProductHeader id={params.id} />
{/* Streamt sobald ready */}
<Suspense fallback={<DetailsSkeleton />}>
<ProductDetails id={params.id} />
</Suspense>
{/* Streamt unabhängig */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={params.id} />
</Suspense>
</div>
);
}
async function ProductDetails({ id }) {
// Diese Funktion blockiert nicht die ganze Page
await new Promise(r => setTimeout(r, 1000)); // Simuliert DB-Call
const details = await fetchProductDetails(id);
return <DetailsCard details={details} />;
}State Management mit Server Components
Lifting State Up to URL
// Server Component liest State aus URL
async function ProductsPage({
searchParams
}: {
searchParams: { sort?: string; filter?: string }
}) {
const products = await prisma.product.findMany({
orderBy: searchParams.sort ? { [searchParams.sort]: 'asc' } : undefined,
where: searchParams.filter
? { category: searchParams.filter }
: undefined
});
return (
<div>
<FilterBar
currentSort={searchParams.sort}
currentFilter={searchParams.filter}
/>
<ProductGrid products={products} />
</div>
);
}
// Client Component für URL-Updates
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
function FilterBar({ currentSort, currentFilter }) {
const router = useRouter();
const searchParams = useSearchParams();
const updateFilter = (key: string, value: string) => {
const params = new URLSearchParams(searchParams);
params.set(key, value);
router.push(`?${params.toString()}`);
};
return (
<div>
<select
value={currentSort}
onChange={(e) => updateFilter('sort', e.target.value)}
>
<option value="name">Name</option>
<option value="price">Preis</option>
</select>
</div>
);
}Server Actions für Mutations
// Server Action
'use server';
import { revalidatePath } from 'next/cache';
async function addToFavorites(productId: string) {
await prisma.favorite.create({
data: {
productId,
userId: getCurrentUserId()
}
});
revalidatePath('/favorites');
}
// Client Component nutzt Server Action
'use client';
import { useTransition } from 'react';
function FavoriteButton({ productId }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
addToFavorites(productId);
});
}}
>
{isPending ? '...' : '❤️'}
</button>
);
}Migration Guide: Client → Server
Schritt 1: Identifiziere reine Daten-Components
// Vorher: Client Component
'use client';
import { useEffect, useState } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return <div>{products.map(p => <Product key={p.id} {...p} />)}</div>;
}
// Nachher: Server Component
async function ProductList() {
const products = await prisma.product.findMany();
return <div>{products.map(p => <Product key={p.id} {...p} />)}</div>;
}Schritt 2: Extrahiere interaktive Teile
// Vorher: Alles Client
'use client';
function ProductCard({ product }) {
const [count, setCount] = useState(1);
return (
<div>
<h2>{product.name}</h2>
<p>{product.description}</p>
<p>{product.price} €</p>
<input
type="number"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
/>
<button onClick={() => addToCart(product.id, count)}>
Kaufen
</button>
</div>
);
}
// Nachher: Server + Client
// ProductCard.tsx (Server)
function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
<p>{product.description}</p>
<p>{product.price} €</p>
{/* Nur interaktiver Teil als Client */}
<AddToCartForm productId={product.id} />
</div>
);
}
// AddToCartForm.tsx (Client)
'use client';
function AddToCartForm({ productId }) {
const [count, setCount] = useState(1);
return (
<>
<input
type="number"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
/>
<button onClick={() => addToCart(productId, count)}>
Kaufen
</button>
</>
);
}Performance-Vorteile
┌─────────────────────────────────────────────────────────────┐
│ BUNDLE SIZE COMPARISON │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client-Only App (alles 'use client'): │
│ ├── React: 40KB │
│ ├── Components: 150KB │
│ ├── Dependencies: 200KB │
│ └── Total: ~390KB JS │
│ │
│ RSC-First App (Server wo möglich): │
│ ├── React: 40KB │
│ ├── Client Components: 30KB │
│ ├── Dependencies (Client only): 50KB │
│ └── Total: ~120KB JS │
│ │
│ Reduktion: 70%! │
│ │
└─────────────────────────────────────────────────────────────┘Fazit
React Server Components transformieren wie wir React-Apps bauen:
- Server-First: Default auf Server, Client nur wo nötig
- Zero Bundle: Server Components senden kein JS
- Direct Access: DB, Filesystem direkt nutzbar
- Composition: Server + Client Components kombinierbar
Das neue Mental Model: "Kann das auf dem Server bleiben?"
Bildprompts
- "Two puzzle pieces fitting together - server and client components, React logo in center"
- "Data flowing from database through server component to client, simplified architecture"
- "Bundle size comparison chart - before and after Server Components, dramatic reduction"