Menu
Nazad na Blog
2 min read
Web Development

React Server Components: Das neue Mental Model für 2026

Umfassendes Verständnis von React Server Components. Composition Patterns, Data Fetching, Client/Server Boundary und Migration von Client-only Apps.

React Server ComponentsRSCServer ComponentsReact 19Next.jsClient Components
React Server Components: Das neue Mental Model für 2026

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

AspektServer ComponentClient Component
**Rendering**ServerServer + Client
**JavaScript**0 bytes zum ClientWird gebundelt
**State**Nein (kein useState)Ja
**Effects**Nein (kein useEffect)Ja
**Event Handlers**NeinJa
**Browser APIs**NeinJa
**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