React context is de officiële manier om data te delen door een componentenboom heen, zonder die op elk niveau handmatig als props door te geven. Als je ooit een overbodige prop hebt toegevoegd aan elk component in een keten, alleen om bijvoorbeeld het thema, de ingelogde gebruiker of de gekozen taal bij een component vijf niveaus diep te krijgen, ben je het probleem tegengekomen dat bekendstaat als "prop drilling." In dit artikel laat ik zien hoe je de Context API correct opzet, in een custom hook verpakt, de meest voorkomende performancevalkuilen vermijdt en herkent wanneer je context juist niet moet gebruiken — allemaal met concrete code.
Wat prop drilling precies is
Stel dat sessie-info bovenaan leeft en alleen een diep geneste Avatar-component die gebruikt. De tussenliggende componenten Layout, Sidebar en UserMenu lezen de waarde nooit, maar zijn gedwongen een prop te accepteren puur om die door te geven:
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
// Layout -> Sidebar -> UserMenu -> Avatar
// elk geeft "user" alleen door aan de volgende eronder
Het probleem: de signatuur van tussenliggende componenten raakt vervuild met data waar ze niet van afhangen, refactoren wordt lastiger, en het hernoemen van één prop golft door de hele keten. Context maakt kortsluiting in die keten: je levert de waarde één keer bovenaan en leest die direct beneden.
Het kleinste werkende voorbeeld
Context bestaat uit drie delen: maak de context met createContext, verspreid een waarde via een Provider, en lees die waarde met useContext.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Wikkel daarna de root van je app in de provider en roep de hook aan vanuit elke afstammeling:
function ThemeButton() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Thema: {theme}</button>;
}
function App() {
return (
<ThemeProvider>
<ThemeButton />
</ThemeProvider>
);
}
Hoeveel lagen er nu ook tussen ThemeButton en App zitten, geen enkele hoeft een theme-prop mee te dragen.
Veilige toegang met een custom hook
Hierboven schreef ik een kleine custom hook genaamd useTheme; geef altijd de voorkeur aan dit patroon. Het heeft twee voordelen: het verbergt de interne werking van de context voor de aanroeper, en het kan een betekenisvolle fout gooien als het zonder provider wordt gebruikt.
export function useTheme() {
const ctx = useContext(ThemeContext);
if (ctx === undefined) {
throw new Error("useTheme moet binnen een <ThemeProvider> gebruikt worden");
}
return ctx;
}
Als je de beginwaarde instelt met createContext(undefined), krijg je bij het vergeten van de provider een duidelijke fout in plaats van stilletjes een verkeerde standaardwaarde. In teamverband versnelt dit het debuggen aanzienlijk.
De grootste performanceval: onnodige re-renders
Het meest misverstane aan context is dit: wanneer de value van de provider verandert, re-renderen alle componenten die die context consumeren. De problemen beginnen wanneer je bij elke render een nieuwe objectreferentie maakt:
// SLECHT: een nieuw { } object per render -> alle consumers re-renderen
<ThemeContext.Provider value={{ theme, toggle }}>
Zelfs als het provider-component om een ongerelateerde reden re-rendert, produceert die regel een nieuw object, en omdat React referenties vergelijkt met Object.is, telt dat als "veranderd." De oplossing is de waarde stabiel te maken met useMemo:
const value = useMemo(() => ({ theme, toggle }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
Een tweede techniek is het splitsen van de context: als je vaak veranderende data (bijv. live tekstinvoer) en zelden veranderende functies (bijv. dispatch) in twee aparte contexts plaatst, raakt de snel veranderende waarde alleen de componenten die haar lezen. Vuistregel: hoe meer ongerelateerde zaken één context draagt, hoe groter het risico op verspillende re-renders.
Combineren met useReducer
Naarmate de state-logica groeit, is useReducer in plaats van useState gebruiken en combineren met context een net patroon. Het gedraagt zich als een kleine globale "store" maar vereist geen externe bibliotheek:
const CartContext = createContext(null);
function cartReducer(state, action) {
switch (action.type) {
case "add": return [...state, action.item];
case "remove": return state.filter(i => i.id !== action.id);
default: return state;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const value = useMemo(() => ({ items, dispatch }), [items]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
React houdt de dispatch-functie stabiel, dus je kunt die veilig gebruiken in de dependency-arrays van memo.
Wanneer je context niet moet gebruiken
Context is niet het antwoord op elk probleem. In de volgende situaties kijk je beter ergens anders:
- Hoogfrequente updates met brede impact (bijv. duizenden abonnees bij elke toetsaanslag): context op zichzelf is geen op performance geoptimaliseerde state-manager; tools als Zustand, Jotai of Redux Toolkit zijn in dit scenario efficiënter.
- Serverdata (van een API, die caching en retries nodig heeft): een datalaag zoals TanStack Query past hier veel beter dan context.
- Maar één à twee niveaus diep: als de prop drilling twee lagen is, is een prop doorgeven vaak leesbaarder dan een context opzetten.
De sweet spot van context is data die zelden verandert maar op veel plekken wordt gelezen: thema, authenticatiestatus, taal/lokalisatie, feature flags.
Veelgestelde vragen
Vervangt context Redux?
Deels. Context is een mechanisme voor "datatransport," geen volwaardige state-managementbibliotheek. Voor simpele globale state is useReducer + context genoeg; maar zodra je middleware, devtools of selector-gebaseerde renderoptimalisatie nodig hebt, verdient Redux Toolkit of Zustand zijn plek.
Is het nesten van meerdere contexts een probleem?
Nee, het is een gangbaar en gezond patroon. Aparte providers voor thema, gebruiker en winkelwagen gebruiken is doorgaans beter voor zowel performance als leesbaarheid dan één gigantische context. Je houdt de root schoon door de geneste providers te bundelen in één AppProviders-component.
Is useMemo echt nodig?
Als de provider-waarde een object of functie bevat en de provider kan re-renderen, ja. Is de waarde een primitief type (string, number, boolean), dan vergelijkt React de waarde in plaats van een referentie en is useMemo niet nodig.
Wil je de state-flow in je React-architectuur vereenvoudigen? Ik help je graag een performante, prop-drilling-vrije context-opzet bouwen — neem contact op.