- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
497 lines
14 KiB
Markdown
497 lines
14 KiB
Markdown
# 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
|
||
|
||
```tsx
|
||
// 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
|
||
|
||
```tsx
|
||
// 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
|
||
|
||
```tsx
|
||
// ❌ 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
|
||
|
||
```tsx
|
||
// ❌ 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
|
||
|
||
```tsx
|
||
// 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
|
||
|
||
```tsx
|
||
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
|
||
|
||
```tsx
|
||
// 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
|
||
|
||
```tsx
|
||
// 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
|
||
|
||
```tsx
|
||
// 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
|
||
|
||
```tsx
|
||
// 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:
|
||
|
||
1. **Server-First**: Default auf Server, Client nur wo nötig
|
||
2. **Zero Bundle**: Server Components senden kein JS
|
||
3. **Direct Access**: DB, Filesystem direkt nutzbar
|
||
4. **Composition**: Server + Client Components kombinierbar
|
||
|
||
Das neue Mental Model: "Kann das auf dem Server bleiben?"
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Two puzzle pieces fitting together - server and client components, React logo in center"
|
||
2. "Data flowing from database through server component to client, simplified architecture"
|
||
3. "Bundle size comparison chart - before and after Server Components, dramatic reduction"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [React Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md)
|
||
- [Next.js Server Components Docs](https://nextjs.org/docs/app/getting-started/server-and-client-components)
|
||
- [Vercel: Understanding React Server Components](https://vercel.com/blog/understanding-react-server-components)
|
||
- [Dan Abramov: RSC from Scratch](https://github.com/reactwg/server-components/discussions/5)
|