Zodra je een project opent met de Next.js App Router, leef je standaard in de wereld van Next.js Server Components. Het oude model waaraan we gewend waren — "elke React-component draait in de browser" — heeft plaatsgemaakt voor een tweedelige architectuur waarin sommige componenten op de server renderen en slechts een deel naar de browser wordt verstuurd. In dit artikel leg ik het echte verschil tussen Server Components en Client Components uit, hoe ze samenwerken, en welke je in het dagelijks werk kiest, met concrete voorbeelden.
Wat zijn Server Components en Client Components?
Het onderscheid draait om waar een component draait.
- Server Component (RSC): draait alleen op de server. De uitvoer wordt naar de browser gestuurd als HTML plus een speciale geserialiseerde payload; de JavaScript van de component zelf gaat niet naar de client. Hij kan rechtstreeks bij de database, het bestandssysteem of geheime API-sleutels.
- Client Component: eenmalig gerenderd op de server en daarna "gehydrateerd" (interactief gemaakt) in de browser.
useState,useEffect, event listeners en browser-API's leven hier.
In de App Router is elke component standaard een Server Component. Om een component een Client Component te maken, voeg je de directive "use client" boven aan het bestand toe.
Wat doet "use client" eigenlijk?
Een veelvoorkomend misverstand is dat een bestand met "use client" "alleen in de browser" draait. In werkelijkheid wordt die component bij het eerste verzoek nog steeds op de server gerenderd; de taak van de directive is het markeren van de client-grens (client boundary) die in dat bestand begint. Vanaf die grens worden de component en wat hij importeert opgenomen in de client-bundel.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Teller: {count}
</button>
);
}
De directive moet helemaal boven aan het bestand staan, vóór elke import. Zodra je een Client Component-grens binnengaat, worden ook alle child-componenten die daarbinnen worden gerenderd aan de clientzijde geëvalueerd.
Welke kies je, en wanneer?
De praktische regel is simpel: laat de standaard een Server Component en schakel pas over naar Client wanneer je interactiviteit of een browser-API nodig hebt.
Kies een Server Component wanneer:
- Je data ophaalt uit een database of een API (rechtstreeks met
async/await). - Je sleutels, tokens of connection strings gebruikt die geheim moeten blijven.
- Je een zware afhankelijkheid (bijvoorbeeld een Markdown-parser of een datumbibliotheek) buiten de client-bundel wilt houden.
- De inhoud grotendeels statisch is, zonder interactie.
Kies een Client Component wanneer:
- Je state (
useState,useReducer) of lifecycle (useEffect) nodig hebt. - Je event listeners zoals
onClickofonChangegebruikt. - Je browser-specifieke API's nodig hebt zoals
window,localStorageofIntersectionObserver. - Je afhankelijk bent van een client-only bibliotheek (veel animatie- of kaartcomponenten, bijvoorbeeld).
Beide samen gebruiken: het compositiepatroon
De echte kracht zit in het nesten van de twee. Het gangbare, aanbevolen patroon is om data op de server op te halen en die als prop door te geven aan een kleine Client Component. Zo houd je het interactieve deel klein en laat je het zware werk op de server.
// app/page.tsx (Server Component)
import Counter from "./counter";
export default async function Page() {
const data = await getData(); // draait op de server
return (
<main>
<h1>{data.title}</h1>
<Counter /> {/* client-grens */}
</main>
);
}
Een belangrijk detail: je kunt een Server Component niet importeren en renderen binnen een Client Component, maar je kunt hem wel als children doorgeven. Zo kun je inhoud van een Server Component tonen binnen een client-wrapper (bijvoorbeeld een theme provider):
// Client Component
"use client";
export default function Panel({ children }) {
return <div className="panel">{children}</div>;
}
// Server Component
<Panel>
<ServerContent /> {/* doorgegeven als children, blijft op de server */}
</Panel>
Veelgemaakte fouten
- Overal
"use client"op zetten. Dit gooit alle voordelen van RSC weg; de bundel groeit en de eerste lading wordt trager. Duw de directive zo dicht mogelijk naar de blad-component (de binnenste interactieve component). - Proberen
useStateof een event handler te gebruiken in een Server Component. Dit geeft een build/runtime-fout; je moet de component een Client Component maken. - Een geheime sleutel naar een Client Component verplaatsen. Alles wat in de client-bundel belandt, is voor iedereen zichtbaar. Geheimen moeten uitsluitend in Server Components of serverzijdige code blijven.
- Een
asyncClient Component schrijven. Server Components mogenasynczijn om data op te halen; om data op te halen in Client Components gebruik jeuseEffectof een bibliotheek zoals SWR.
Veelgestelde vragen
Hebben Server Components getServerSideProps vervangen?
Grotendeels wel. De App Router heeft geen getServerSideProps of getStaticProps meer; je haalt data rechtstreeks op met await binnen een async Server Component. Het cache- en revalidatiegedrag regel je vervolgens via de opties van fetch.
Kan een Client Component een Server Component als child krijgen?
Ja. Een Client Component kan een Server Component niet rechtstreeks importeren en renderen, maar kan er wel een ontvangen via children of een andere prop. Dit is de standaardmanier om interactieve wrappers te combineren met server-gerenderde inhoud.
Waarom werkt useState niet in een Server Component?
Omdat useState state nodig heeft die in de browser leeft en tussen renders behouden blijft; een Server Component heeft geen lifecycle aan de clientzijde. Wanneer je state of interactiviteit nodig hebt, splits je dat stuk af naar een Client Component.
Wil je je Next.js-architectuur goed opzetten? Ik help je een snelle, onderhoudbare app te bouwen die Server en Client Components goed in balans houdt. Neem contact op.