De meeste React-performancetutorials eindigen met hetzelfde slechte advies: wikkel alles in useMemo en useCallback en dat was het. Als je dat advies hebt opgevolgd, heb je je applicatie waarschijnlijk trager gemaakt. Deze hooks zijn niet gratis. Elke hook reserveert geheugen, vergelijkt dependencies en slaat gecachte waarden op. Zonder doel gebruikt, worden ze overhead in plaats van optimalisatie.

Laten we dit terugbrengen tot wat er echt toe doet.

Wat elke hook echt doet

useMemo onthoudt een waarde. Je geeft het een functie die zwaar werk verricht, en het geeft het resultaat terug. Bij de volgende render, als je dependencies niet zijn veranderd, slaat React de berekening over en geeft het oude resultaat terug.

useCallback onthoudt een functie. Het voert de functie niet voor je uit. Het geeft simpelweg dezelfde functie-instantie terug tussen renders door, zolang de dependencies hetzelfde blijven.

Dat is het volledige verschil. De ene cachet een berekende waarde. De andere cachet een referentie. Het door elkaar halen hiervan leidt tot code die geoptimaliseerd lijkt, maar zich identiek gedraagt als niet-gewikkelde code, terwijl het extra geheugen kost.

Waarom functie-identiteit je componentboom breekt

Wanneer een component opnieuw rendert, voert React de volledige functiebody opnieuw uit. Elke variabele wordt opnieuw aangemaakt. Elke inline functie krijgt een gloednieuw adres in het geheugen.

In JavaScript zijn twee functies die exact dezelfde logica bevatten niet gelijk aan elkaar. () => {} === () => {} evalueert naar false. Dezelfde regel geldt voor objecten en arrays. Als je parent-component handleSubmit definieert en deze doorgeeft aan een child, ontvangt die child bij elke render een nieuwe prop. Zelfs als de child is gewikkeld in React.memo, kan deze niet zien dat de nieuwe functie hetzelfde doet als de oude. De referentie is veranderd, dus de child rendert opnieuw.

Dit is het kernprobleem dat useCallback is ontworpen om op te lossen. Het gaat niet om snelheid. Het gaat om stabiliteit.

Wanneer useMemo zijn waarde bewijst

Je hebt useMemo nodig wanneer je werk verricht dat objectief duur is en waarbij je een meetbare vertraging ziet.

Denk aan het filteren van een enorme dataset. Als je een tabel hebt met tienduizenden rijen en een zoekveld, schrijf je misschien zoiets in je component:

const visibleRows = rows.filter(r => r.name.includes(query));

Zonder useMemo wordt die loop bij elke render uitgevoerd. Als de gebruiker op een knop klikt die een zijbalk in- of uitschakelt, rendert de parent opnieuw, en wordt je filter opnieuw uitgevoerd, zelfs als rows en query nooit zijn veranderd. Bij een grote dataset is die hapering zichtbaar.

useMemo lost dit op door het resultaat vast te leggen:

const visibleRows = useMemo(() => {
  return rows.filter(r => r.name.includes(query));
}, [rows, query]);

Nu voert React die filter alleen opnieuw uit wanneer de dependencies daadwerkelijk veranderen.

Dezelfde logica geldt voor complexe wiskundige berekeningen, het transformeren van API-responses naar chart-vriendelijke formaten, of het afleiden van state die anders constant opnieuw berekend zou worden.

Er is een tweede, minder voor de hand liggende use case. Als je lokaal een object of array aanmaakt en deze opneemt in een useEffect dependency array, kun je per ongeluk die effect bij elke render triggeren. Inline objecten en arrays krijgen elke keer een nieuwe identiteit, waardoor het effect een veranderde dependency ziet en opnieuw wordt uitgevoerd. Het memoïseren van dat object met useMemo houdt de referentie stabiel en zorgt ervoor dat je effect alleen draait wanneer de onderliggende data daadwerkelijk verandert.

Wanneer useCallback noodzakelijk wordt

useCallback is het belangrijkst wanneer je handlers doorgeeft aan child-componenten die zijn geoptimaliseerd met React.memo.

Stel je een parent-component voor die een teller bijhoudt. Deze rendert ook een dure child-lijst:

function Parent() {
  const [count, setCount] = useState(0);
  
  const handleItemClick = (id) => {
    console.log(id);
  };
  
  return (
    <div>
      <button onClick={() => setCount(c + 1)}>{count}</button>
      <ExpensiveList onItemClick={handleItemClick} />
    </div>
  );
}

Elke keer dat count verandert, rendert Parent opnieuw. Er wordt een nieuwe handleItemClick aangemaakt. Omdat ExpensiveList een nieuwe prop-referentie ontvangt, rendert deze ook opnieuw. Als ExpensiveList is gewikkeld in React.memo, is die memoïsering volledig verspild omdat de functie-prop is veranderd.

useCallback behoudt de referentie:

const handleItemClick = useCallback((id) => {
  console.log(id);
}, []);

Nu rendert ExpensiveList alleen opnieuw wanneer dat echt nodig is.

Een andere kritieke situatie heeft betrekking op useEffect. Als een effect zich abonneert op een functie die binnen je component is gedefinieerd, en die functie bij elke render van identiteit verandert, zal het effect herhaaldelijk worden afgebroken en opnieuw worden aangemeld. Het memoïseren van de functie houdt het effect stabiel.

De dependency array-valstrik en stale closures

Beide hooks vertrouwen op dependency arrays, en dit is waar de meeste bugs zich verschuilen.

Als je een variabele weglaat uit de dependency array, sluit je gememoïseerde functie of waarde zich af op een oude versie van die variabele. Dit is een stale closure. De UI toont misschien verse data, maar je callback kijkt nog steeds naar de state van drie renders geleden. De oplossing is simpel, maar wordt tijdens code reviews gemakkelijk over het hoofd gezien: neem elke waarde die binnen de hook wordt gebruikt en die kan veranderen, op.

Voer de react-hooks/exhaustive-deps ESLint-regel uit. Deze vangt overduidelijke weglatingen op. Maar behandel het niet als een robot. Begrijp waarom elke dependency belangrijk is.

De verborgen belasting van over-optimalisatie

Beginners beschermen vaak elke functie en elke waarde met deze hooks omdat dat veilig voelt. Die gewoonte werkt echter averechts.

React moet gecachte waarden in het geheugen opslaan. Bij elke render moet het door je dependency array itereren en elk item vergelijken met behulp van Object.is. Die vergelijking is goedkoop, maar niet gratis. Als je een triviale event handler zoals onClick={() => setOpen(true)} in een useCallback verpakt, betaal je geheugen- en CPU-kosten om het aanmaken van een functie te voorkomen die onmiddellijk gealloceerd zou zijn.

De hooks zorgen ook voor ruis. Code die is verpakt in useMemo en useCallback is moeilijker te lezen en te onderhouden. Elke dependency array is een potentiële stale closure die wacht om je te bijten.

De echte vuistregel is niet glamoureus maar wel effectief: schrijf eerst gewone code. Optimaliseer pas als je bewijs hebt van een probleem. Gebruik de React DevTools Profiler om te identificeren welke componenten duur zijn en welke renders verspillend zijn. Als een render minder dan een paar milliseconden duurt, zal geen enkele gebruiker dat merken, en lost je memoïzatie niets op.

De conclusie

useMemo is voor dure waarden. useCallback is voor stabiele functie-referenties. Geen van beide hooks zorgt ervoor dat je component sneller rendert op zichzelf; ze voorkomen onnodig downstream werk. Begin zonder deze hooks, meet met echte tooling, en voeg ze precies toe waar de profiler een bottleneck laat zien. Schone code die af en toe opnieuw rendert, zal bijna altijd winnen van over-engineered code die alles gememoïseert.