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>
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user