Files
Portfolio/blog-posts/29-react-server-components.md
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- 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>
2026-01-25 11:42:11 +01:00

497 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)