Een React custom hook is de officiële manier om herhaalde logica uit je componenten te halen en in één herbruikbare functie te plaatsen. Als je merkt dat je dezelfde combinatie van useState + useEffect naar drie afzonderlijke componenten hebt gekopieerd, is dat precies het moment om die naar een hook te verplaatsen. In deze gids leg ik aan de hand van echte voorbeelden uit wat een custom hook is, hoe je er een schrijft en wanneer het zich echt loont.
Wat een custom hook wel en niet is
Een custom hook is een gewone JavaScript-functie waarvan de naam met use begint en die binnenin andere hooks mag aanroepen (useState, useEffect, useRef, zelfs andere custom hooks). Hij heeft geen magische eigenschap; React verwacht eenvoudigweg dat elke functie met het use-voorvoegsel zich aan de regels van hooks houdt.
- Hij deelt logica, geen state. Als twee componenten dezelfde hook gebruiken, krijgt elk zijn eigen onafhankelijke state.
- Hij geeft data, functies of een combinatie daarvan terug — nooit JSX.
- Het is geen component; hij rendert niet, hij wordt alleen vanuit een component (of een andere hook) aangeroepen.
Het duidelijkste onderscheid is dit: is je reden om iets af te splitsen "de UI hergebruiken", haal er dan een component uit; is het "de logica hergebruiken", haal er dan een hook uit.
De twee regels: waar je ze mag aanroepen
Hooks, of ze nu custom of ingebouwd zijn, gehoorzamen aan twee regels, en die overtreden maakt je app onvoorspelbaar:
- Roep ze alleen op het hoogste niveau aan. Roep nooit een hook aan binnen een lus, conditie of geneste functie. Zo kan React erop vertrouwen dat hooks bij elke render in dezelfde volgorde draaien.
- Roep ze alleen vanuit React-functies aan. Dus vanuit componenten of vanuit andere custom hooks. Roep geen hook aan vanuit een gewone hulpfunctie.
Installeer eslint-plugin-react-hooks om deze regels automatisch af te dwingen; het vangt verkeerd gebruik al op terwijl je nog typt.
Onze eerste custom hook: useToggle
Laten we beginnen met iets eenvoudigs dat je toch voortdurend nodig hebt. Dropdowns, modals, "meer tonen" — overal moet je een boolean omkeren:
import { useState, useCallback } from 'react';
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
Het gebruik in een component is heerlijk schoon:
function Sidebar() {
const { value: isOpen, toggle, setFalse } = useToggle();
return (
<>
<button onClick={toggle}>Menu</button>
{isOpen && <nav onMouseLeave={setFalse}>...</nav>}
</>
);
}
We verpakken de teruggegeven functies in useCallback om te voorkomen dat ze bij elke render als een nieuwe referentie worden aangemaakt; zo kun je ze veilig als useEffect-afhankelijkheden gebruiken.
Een hook met neveneffecten: useLocalStorage
De echte kracht komt naar voren zodra useEffect en browser-API's in beeld komen. Een hook die een waarde gesynchroniseerd houdt met localStorage brengt logica die anders over tientallen componenten zou worden herhaald samen op één plek:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = window.localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
// quota overschreden of privémodus: stilletjes negeren
}
}, [key, value]);
return [value, setValue];
}
Twee details zijn hier belangrijk. Ten eerste hebben we een functie aan useState doorgegeven (een lazy initializer), zodat het lezen van localStorage alleen bij de eerste render draait en niet bij elke render. Ten tweede hebben we zowel JSON.parse als de toegang tot localStorage in een try/catch verpakt, omdat beschadigde data of een privétabblad dat toegang weigert in productie een heel reële bron van bugs is.
Een data-ophaalhook met opruimen
De meest voorkomende fout bij asynchroon ophalen is proberen state bij te werken nadat het component is gedemonteerd (unmount). AbortController lost dit netjes op:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [state, setState] = useState({ data: null, error: null, loading: true });
useEffect(() => {
const controller = new AbortController();
setState({ data: null, error: null, loading: true });
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setState({ data, error: null, loading: false }))
.catch(error => {
if (error.name !== 'AbortError') {
setState({ data: null, error, loading: false });
}
});
return () => controller.abort();
}, [url]);
return state;
}
De opruimfunctie die de effect teruggeeft, breekt het vorige verzoek af telkens wanneer url verandert of het component wordt gedemonteerd. Zo voorkom je zowel race conditions als de bekende waarschuwing "Can't perform a React state update on an unmounted component".
Wanneer een hook afsplitsen, en wanneer wachten
Voortijdige abstractie is een echte valkuil. Een praktische set regels:
- Splits een hook af zodra je dezelfde logica op minstens twee plaatsen gebruikt (of er zeker van bent dat je dat zult doen).
- Als de effect- en state-logica binnen één component lang genoeg wordt om de leesbaarheid te schaden, is afsplitsen alleen al voor de testbaarheid de moeite waard.
- Een
useFetchschrijven is leerzaam, maar als je in productie caching, opnieuw proberen en invalidatie nodig hebt, is een volwassen bibliotheek zoals TanStack Query meestal de betere keuze. - Ontwerp de vorm die je hook teruggeeft als een duidelijk contract: beslis bewust of je een array teruggeeft (positioneel, zoals
useState) of een object (benoemd, bij meerdere velden).
Veelgestelde vragen
Wat is het verschil tussen een custom hook en een gewone functie?
Een custom hook mag binnenin andere hooks zoals useState en useEffect aanroepen en haakt in op de render-levenscyclus van React; een gewone hulpfunctie kan dat niet. In de praktijk wordt het onderscheid aangegeven door het use-voorvoegsel, en de linter handhaaft zijn regels op basis daarvan.
Als twee componenten dezelfde hook gebruiken, delen ze dan de state?
Nee. De hook deelt alleen de logica; elk component krijgt zijn eigen geïsoleerde state. Wil je de state echt delen, dan heb je Context of een state-managementoplossing nodig.
Hoe test ik custom hooks?
Met de renderHook-helper van React Testing Library kun je de hook geïsoleerd renderen en de teruggegeven waarden en binnen act getriggerde updates controleren. Bevat de hook pure logica, dan kun je die ook in een aparte functie afsplitsen en rechtstreeks testen.
Wil je je herhaalde React-logica naar nette hooks verplaatsen? Laten we samenwerken aan je frontend-architectuur en React-projecten — neem contact met me op.