Initial commit: Portfolio Website

Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 15:07:20 +01:00
co-authored by Claude Opus 4.5
commit e1bbe5455d
315 changed files with 94124 additions and 0 deletions
+496
View File
@@ -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)