Files
Portfolio/blog-posts/29-react-server-components.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d 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>
2026-02-01 15:07:20 +01:00

14 KiB
Raw Blame History

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:

  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