React Server Components in productie: patronen en valkuilen

React Server Components in productie: beproefde patronen, veelvoorkomende anti-patronen en debugstrategieën voor robuuste Next.js 15-applicaties.

Patronen en valkuilen van React Server Components in productie

React Server Components (RSC) veranderen fundamenteel hoe server-rendering werkt in Next.js 15, maar productie-adoptie onthult valkuilen die de officiële documentatie niet altijd dekt. Dit artikel ontleedt de patronen die werken, de patronen die breken, en hoe problemen te diagnosticeren voordat ze in productie belanden.

Server Components vs Client Components

Een Server Component draait uitsluitend op de server en stuurt nul JavaScript naar de browser. Een Client Component (gemarkeerd met "use client") draait aan beide kanten. De regel: houd Client Components zo klein en zo laag mogelijk in de boom.

De server-client-grens: het boundary-patroon begrijpen

De meest voorkomende RSC-valkuil betreft de grens tussen Server- en Client Components. Zodra een component de "use client"-directive draagt, worden alle geïmporteerde kinderen ook Client Components, zelfs zonder de directive.

ProductPage.tsx (Server Component)tsx
import { ProductDetails } from './ProductDetails'
import { AddToCartButton } from './AddToCartButton'

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const product = await getProduct(id)

  return (
    <div>
      {/* Server Component: directe DB-toegang */}
      <ProductDetails product={product} />
      {/* Client Component: geïsoleerde interactiviteit */}
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  )
}
AddToCartButton.tsx (Client Component)tsx
'use client'

import { useState } from 'react'

export function AddToCartButton({ productId, price }: { productId: string; price: number }) {
  const [adding, setAdding] = useState(false)

  async function handleAdd() {
    setAdding(true)
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, quantity: 1 }),
    })
    setAdding(false)
  }

  return (
    <button onClick={handleAdd} disabled={adding}>
      {adding ? 'Toevoegen...' : `Toevoegen aan winkelwagen — $${price}`}
    </button>
  )
}

Het kernpatroon: data doorgeven als serializeerbare props van het Server Component naar het Client Component. Functies, klassen en Date-objecten kunnen deze grens niet passeren.

Anti-patroon: de overbodige Client Component-wrapper

Een veelgemaakte fout is een Client Component maken dat Server Component-kinderen omhult, waardoor de hele subboom client-side wordt geforceerd.

PageWrapper.tsx — ANTI-PATROONtsx
'use client'

import { useState } from 'react'

// Alle kindcontent wordt client-side
export function PageWrapper({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
  return (
    <div className={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Thema wisselen
      </button>
      {children}
    </div>
  )
}
children als slot

De oplossing: Server Components doorgeven als children (slot-patroon). Kinderen die als props worden doorgegeven blijven Server Components, zelfs wanneer de parent een Client Component is. De code hierboven werkt correct zolang children afkomstig is van een Server Component-parent.

layout.tsx (Server Component)tsx
import { PageWrapper } from './PageWrapper'
import { HeavyServerContent } from './HeavyServerContent'

export default function Layout() {
  return (
    <PageWrapper>
      {/* Blijft Server Component ondanks de client-wrapper */}
      <HeavyServerContent />
    </PageWrapper>
  )
}

Dit compositiepatroon behoudt de voordelen van server-rendering voor zware content terwijl interactiviteit op wrapper-niveau mogelijk wordt.

Asynchrone dataverwerking: het fetch-in-component-patroon

React 19 en Next.js 15 ondersteunen async/await direct in Server Components. Dit patroon vereenvoudigt het ophalen van data ten opzichte van de oudere getServerSideProps-aanpak.

UserProfile.tsx (Server Component)tsx
import { cache } from 'react'

// Dedupliceert identieke aanroepen binnen dezelfde render
const getUser = cache(async (userId: string) => {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 3600 }, // Cache voor 1 uur
  })
  if (!res.ok) throw new Error('User not found')
  return res.json()
})

export default async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId)

  return (
    <section>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>Lid sinds {new Date(user.createdAt).toLocaleDateString('nl-NL')}</p>
    </section>
  )
}

Drie kritieke punten:

  • Reacts cache() dedupliceert identieke aanroepen tijdens een enkele server-render
  • next: { revalidate } regelt de cacheduur aan de Next.js-kant
  • Fouten in een asynchroon Server Component triggeren de dichtstbijzijnde error.tsx

Serialisatie-valkuil: wat de grens niet passeert

Data die wordt uitgewisseld tussen Server- en Client Components moet JSON-serializeerbaar zijn. Dit veroorzaakt stille fouten of crashes.

tsx
// VALKUIL: niet-serializeerbare types doorgeven
// Functie — werkt niet
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// Gebruik in plaats daarvan een geïmporteerde Server Action
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />

// Date-object — werkt niet
<ClientComp createdAt={new Date()} />
// ISO-string — werkt
<ClientComp createdAt={new Date().toISOString()} />

// Map, Set, RegExp — werkt niet
<ClientComp data={new Map([['key', 'value']])} />
// Plain object of array — werkt
<ClientComp data={{ key: 'value' }} />

Server Actions (functies gemarkeerd met "use server") zijn de uitzondering: ze kunnen als props worden doorgegeven aan een Client Component omdat Next.js ze omzet in HTTP-endpoints.

Klaar om je React / Next.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Streaming en Suspense: progressieve laadpatronen

SSR-streaming met Suspense stuurt HTML progressief naar de browser. Het optimale patroon gebruikt granulaire Suspense-boundaries rondom elke asynchrone sectie.

DashboardPage.tsx (Server Component)tsx
import { Suspense } from 'react'
import { RevenueChart } from './RevenueChart'
import { RecentOrders } from './RecentOrders'
import { UserStats } from './UserStats'

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-6">
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>

      <Suspense fallback={<StatsSkeleton />}>
        <UserStats />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

Elke sectie laadt onafhankelijk. Als RevenueChart 3 seconden duurt en UserStats 200 ms, verschijnen de stats direct zonder op de grafiek te wachten.

Suspense en SEO

Content binnen een Suspense-boundary wordt server-side gerenderd en opgenomen in de initiële HTML. Crawlers zien de volledige content. Streaming beïnvloedt alleen de leveringssnelheid naar de browser, niet de SEO-zichtbaarheid.

Productie-debugging: RSC-problemen traceren

RSC-fouten zijn vaak cryptisch. Drie diagnostische technieken werken in productie.

1. Hydration-mismatches identificeren

debug-hydration.tsxtsx
'use client'

import { useEffect, useState } from 'react'

export function HydrationDebug() {
  const [isClient, setIsClient] = useState(false)

  useEffect(() => {
    setIsClient(true)
  }, [])

  if (process.env.NODE_ENV !== 'development') return null

  return (
    <div style={{ position: 'fixed', bottom: 0, right: 0, padding: '4px 8px', fontSize: 12 }}>
      {isClient ? 'Client' : 'Server'}
    </div>
  )
}

2. De RSC-payload loggen

In Next.js 15 RSC-logging activeren in next.config.ts:

next.config.tstypescript
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true, // Toont volledige fetch-URL's
    },
  },
}

export default nextConfig

3. Payload-grootte controleren

Een te grote RSC-payload (> 128 KB) verslechtert de prestaties. Monitor netwerkverzoeken met het content type text/x-component in DevTools.

Geavanceerd patroon: compositie met Server Actions

Server Actions gecombineerd met Server Components creëren een natuurlijk CQRS-patroon: leesoperaties op de server (RSC), schrijfoperaties via actions.

TodoList.tsx (Server Component)tsx
import { getTodos } from '@/lib/services/todo'
import { TodoForm } from './TodoForm'
import { deleteTodo } from '@/lib/actions/todo'

export default async function TodoList() {
  const todos = await getTodos()

  return (
    <div>
      <TodoForm />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.title}
            <form action={deleteTodo}>
              <input type="hidden" name="id" value={todo.id} />
              <button type="submit">Verwijderen</button>
            </form>
          </li>
        ))}
      </ul>
    </div>
  )
}
actions/todo.tstsx
'use server'

import { revalidatePath } from 'next/cache'
import { TodoService } from '@/lib/services/todo'

export async function deleteTodo(formData: FormData) {
  const id = formData.get('id') as string
  await TodoService.delete(id)
  revalidatePath('/todos')
}

De revalidatePath-aanroep activeert een nieuwe Server Component-render met bijgewerkte data, zonder de hele pagina te herladen.

Voor diepere sollicitatievoorbereiding op deze onderwerpen, bekijk de Next.js Server Actions-module en de Next.js Data Fetching-module op SharpSkill. De officiële React-documentatie behandelt de volledige Server Components-specificatie.

Conclusie

  • Houd Client Components klein en geïsoleerd onderaan de componentenboom
  • Gebruik het slot-patroon (children) om Server Components binnen een client-wrapper te behouden
  • Verifieer altijd de serializeerbaarheid van props die de server-client-grens passeren
  • Plaats granulaire Suspense-boundaries rondom elke onafhankelijke asynchrone sectie
  • Monitor RSC-payloadgroottes in productie (doel < 128 KB)
  • Combineer Server Components (lezen) en Server Actions (schrijven) voor een natuurlijk CQRS-patroon
  • Gebruik Reacts cache() om verzoeken binnen een enkele server-render te dedupliceren

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#react server components
#next.js 15
#rsc patterns
#production
#react 19

Delen

Gerelateerde artikelen