Ga naar inhoud
·4 min

React Server Components in de praktijk: lessen na 6 maanden

Wat ik heb geleerd over RSC na zes maanden bouwen met Next.js 16. Over de voordelen, de valkuilen en wanneer je toch een Client Component nodig hebt.

ReactNext.jsServer ComponentsPerformance

Zes maanden geleden begon ik aan een nieuw project met Next.js 16 en React Server Components als uitgangspunt. Inmiddels draait het in productie en heb ik een duidelijk beeld van wat werkt, wat niet werkt, en waar de documentatie tekortschiet.

De belofte vs. de realiteit

React Server Components beloven een wereld waarin je minder JavaScript naar de client stuurt, directe toegang hebt tot je database vanuit componenten, en streaming UI kunt bouwen. Die belofte is grotendeels waar — maar de weg ernaartoe heeft verrassingen.

Hoe Server en Client Components samenwerken in Next.js
0%
Kleinere client bundle
0 bytes
JS voor Server Components
0%
Minder API routes nodig
0x
Snellere initiële load
De impact van Server Components op mijn productie-app na 6 maanden

Wat direct geweldig werkt

Minder client-side JavaScript. Mijn bundlesize daalde met ~35% toen ik componenten die geen interactiviteit nodig hadden naar server-only verplaatste. Geen useState, geen useEffect, geen hydration — gewoon HTML die de server stuurt.

// Server Component — 0 bytes JS naar de client
async function ProjectList() {
  const projects = await db.project.findMany({
    where: { published: true },
    orderBy: { createdAt: "desc" },
  });
 
  return (
    <div className="grid gap-6 md:grid-cols-3">
      {projects.map((project) => (
        <ProjectCard key={project.id} {...project} />
      ))}
    </div>
  );
}

Database queries in componenten. Geen API routes nodig voor simpele data-fetching. De component vraagt de data, rendert het, en stuurt HTML. Dit elimineert een hele laag boilerplate.

Streaming met Suspense. Zware queries blokkeren niet de hele pagina. Wrap het in een Suspense boundary en de gebruiker ziet direct content terwijl de rest laadt.

Traditioneel: alles client-side

  • Alle componenten in de JS bundle
  • Hydration voor elke component
  • API routes nodig voor data fetching
  • Loading states handmatig beheren
  • Grote client-side bundel
VS

Server-first: RSC aanpak

  • Alleen interactieve delen in bundle
  • Server Components: 0 bytes JS
  • Database queries direct in componenten
  • Streaming met Suspense boundaries
  • Minimale client-side JavaScript
De paradigmaverschuiving: van alles-client naar server-first
Bundle size vergelijking: Server Components reduceren de client-side JavaScript drastisch

De valkuilen

1. De "use client" grens is besmettelijk

Zodra je een component markeert als "use client", zijn alle kinderen ook client components. Dit dwingt je om je componentenboom anders te structureren dan je gewend bent:

// ❌ Hele pagina wordt client component
"use client";
export default function Dashboard() {
  const [tab, setTab] = useState("overview");
  return (
    <div>
      <Tabs value={tab} onChange={setTab} />
      <ExpensiveDataGrid /> {/* Wordt ook client component */}
    </div>
  );
}
 
// ✅ Alleen de interactieve delen zijn client components
export default function Dashboard() {
  return (
    <div>
      <TabSwitcher /> {/* Client component — bevat useState */}
      <Suspense fallback={<Skeleton />}>
        <ExpensiveDataGrid /> {/* Server component — haalt data op */}
      </Suspense>
    </div>
  );
}

De truc is om interactieve elementen zo klein mogelijk te houden en ze als bladeren in je componentenboom te plaatsen, niet als wortels.

2. Serialisatiegrenzen zijn streng

Je kunt geen functies, classes, of Date-objecten doorgeven van server naar client components. Alleen serialiseerbare data: strings, numbers, booleans, arrays, plain objects.

// ❌ Werkt niet — functie is niet serialiseerbaar
<ClientButton onClick={() => deleteProject(id)} />
 
// ✅ Gebruik Server Actions
<ClientButton action={deleteProject.bind(null, id)} />

Dit voelt in het begin beperkend, maar het dwingt een schoner patroon af: data gaat naar beneden, acties gaan via Server Actions.

3. Caching is complex

Next.js heeft meerdere caching-lagen: Request Memoization, Data Cache, Full Route Cache, Router Cache. Ze werken samen, maar het begrijpen van wanneer welke cache actief is — en hoe je ze invalideert — kost tijd.

🔄

Request Memoization

Deduplicatie van dezelfde fetch calls binnen één render pass

💾

Data Cache

Persistente cache voor fetch resultaten op de server

📄

Full Route Cache

Volledige HTML + RSC payload op build-time gecached

🧭

Router Cache

Client-side cache van bezochte routes voor snelle navigatie

De 4 caching-lagen van Next.js — van server-side deduplicatie tot client-side route cache

Caching valkuil

De Router Cache kan onverwacht oude data tonen na een Server Action. Gebruik altijd revalidatePath() of revalidateTag() na mutaties om alle caching-lagen te invalideren.

Mijn vuistregel:

  • Statische pagina's: Laat Next.js cachen, gebruik revalidatePath() na mutaties
  • Dynamische data: Gebruik unstable_noStore() of zet de route op dynamic = "force-dynamic"
  • Gebruikersspecifieke data: Altijd dynamisch, nooit cachen

4. Testing is lastiger

Server Components kun je niet zomaar renderen in een test met React Testing Library. Je hebt ofwel integratietests nodig die de hele server-rendering pipeline simuleren, of je test de server- en clientdelen apart.

Mijn aanpak:

Server Components → Unit test de data-logica apart
Client Components → React Testing Library
Volledige pagina → E2E met Playwright
📄Page (Server)🏗️Layout (Server)📋Sidebar (Server)🔍Search (Client)📝Content (Server)📊DataGrid (Server)🔀Tabs (Client)💬Modal (Client)
Component tree: groene nodes zijn Server Components (0 bytes JS), rode nodes zijn Client Components

Patronen die werken

Composition pattern voor interactiviteit

Composition pattern

Houd Client Components zo klein mogelijk en plaats ze als bladeren in je componentenboom. Geef ze door als children aan Server Components — zo profiteer je maximaal van server-side rendering.

Geef interactieve client components door als children aan server components:

// Server component
async function Sidebar() {
  const items = await getNavItems();
  return (
    <nav>
      <SidebarSearch /> {/* Client component */}
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.label}</li>
        ))}
      </ul>
    </nav>
  );
}

Server Actions voor mutaties

Server Actions vervangen API routes voor formulierverwerking en data-mutaties:

async function createContact(formData: FormData) {
  "use server";
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;
 
  await db.contact.create({ data: { name, email } });
  revalidatePath("/contacts");
}
 
// In je component
<form action={createContact}>
  <input name="name" required />
  <input name="email" type="email" required />
  <button type="submit">Toevoegen</button>
</form>

Geen fetch calls, geen API routes, geen loading state management. Het formulier werkt zelfs zonder JavaScript — progressive enhancement ingebouwd.

Parallel data fetching

Meerdere async componenten in dezelfde layout fetchen hun data parallel, niet sequentieel:

export default function DashboardLayout({ children }) {
  return (
    <div className="grid grid-cols-[240px_1fr]">
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar /> {/* Fetcht navigatie-items */}
      </Suspense>
      <div>
        <Suspense fallback={<HeaderSkeleton />}>
          <Header /> {/* Fetcht user data */}
        </Suspense>
        {children} {/* Pagina-specifieke data */}
      </div>
    </div>
  );
}

Wanneer je RSC wel en niet moet gebruiken

Gebruik Server Components voor:

  • Data-fetching en -weergave
  • Statische content (marketing pages, blog posts)
  • SEO-kritische pagina's
  • Components die geen browser APIs nodig hebben

Gebruik Client Components voor:

  • Formulieren met realtime validatie
  • Interactieve UI (drag & drop, animaties, modals)
  • Components die useState, useEffect, of browser APIs gebruiken
  • Third-party libraries die client-side rendering vereisen

Conclusie

RSC is geen silver bullet, maar het is een fundamentele verbetering in hoe we React-applicaties bouwen. De initiële leercurve is steil, maar na een paar weken voelt de scheiding tussen server en client natuurlijk aan.

Het belangrijkste inzicht: denk niet in "server vs. client components", maar in "waar heeft de gebruiker interactiviteit nodig?" Begin server-first en voeg client boundaries toe waar nodig.

Blijf op de hoogte

Ontvang nieuwe artikelen direct in je inbox. Geen spam, alleen waardevolle content.

Gerelateerde artikelen

Meer over dit onderwerp

Plan een gesprek