Heb je ooit een tooltip gebouwd die vanuit de linkerbovenhoek naar zijn juiste plek verspringt? Of een modal die even de verkeerde grootte heeft voordat hij op zijn plek valt? Die fractie van een seconde aan glitch is een layout-flikkering. Dit gebeurt wanneer React de DOM leest, een correctie berekent en de state bijwerkt, maar de browser al begonnen is met het renderen van pixels op het scherm. Het gebruikelijke advies is om useEffect te vervangen door useLayoutEffect. De swap werkt, maar alleen als je precies begrijpt wanneer elke hook wordt uitgevoerd binnen de browser-pipeline.

De Browser-pipeline: Render, Commit, Paint

React werkt een component bij in drie afzonderlijke fasen. In de render-fase bouwt — of herbouwt — React de Virtual DOM en berekent de diff. Er vinden nog geen werkelijke pixelwijzigingen plaats; dit is pure berekening die in het geheugen plaatsvindt. Daarna volgt de commit-fase, waarin React die wijzigingen toepast op de echte DOM-nodes. Stijlen worden bijgewerkt, nodes worden toegevoegd of verwijderd, en tekst verandert.

Vervolgens neemt de browser het over. In de paint-fase berekent de rendering engine van de browser de layout-geometrie en tekent pixels op het scherm. Deze volgorde is rigide. De browser moet de layout voltooien voordat hij kan painten, en hij moet klaar zijn met painten voordat de gebruiker iets nieuws ziet. Het gat tussen commit en paint wordt gemeten in milliseconden, maar het is echt, en het is het venster waarin useEffect en useLayoutEffect uiteenlopen.

Waarom useEffect de flikkering veroorzaakt

useEffect draait asynchroon; het is gepland om uit te voeren nadat de browser het scherm al heeft gepaintd. De DOM is bijgewerkt, de pixels zijn getekend, en dan komt React weer in actie om je effect uit te voeren.

Stel je voor dat je een dropdownmenu onder een knop rendert. Binnen useEffect roep je buttonRef.current.getBoundingClientRect() aan, bereken je de juiste top- en left-coördinaten en sla je deze op in de state. Omdat useEffect pas na de paint draait, heeft de browser het dropdownmenu al getekend op de standaardpositie, bijvoorbeeld op top: 0, left: 0. Pas na die paint werkt jouw effect de state bij. React commit de gecorrigeerde coördinaten, en de browser paindt opnieuw. De gebruiker ziet twee frames: de verkeerde positie, en daarna de juiste. Die visuele sprong is de flikkering die iedereen probeert te vermijden.

Voor het ophalen van data, API-calls, analytics-tracking of het instellen van event listeners maakt deze vertraging niet uit. De gebruiker geeft er niet om of een analytics-beacon enkele milliseconden na de paint wordt afgevuurd. Sterker nog, door niet-visueel werk uit te stellen tot na de paint, blijft de initiële render responsief. Maar voor layout-afhankelijke correcties is useEffect simpelweg te laat.

Hoe useLayoutEffect de paint blokkeert

useLayoutEffect draait synchroon, direct nadat React de DOM heeft gemuteerd maar voordat de browser de kans krijgt om de layout te berekenen of pixels te painten. Het blokkeert de volledige paint-pipeline.

Als je dezelfde meting van het dropdownmenu uitvoert binnen useLayoutEffect, verandert de volgorde. React commit de initiële DOM-update, voert je layout-effect uit, en jouw state-update triggert een synchrone re-render. React commit de gecorrigeerde coördinaten, en pas daarna gaat de browser painten. De gebruiker ziet één frame, dat al correct is.

Dat blokkerende gedrag is zowel de feature als het risico. Omdat useLayoutEffect voorkomt dat de browser gaat painten totdat het klaar is, bevriest elke zware berekening daarbinnen de UI. Zelfs een paar dozijn milliseconden aan geblokkeerde paint voelt voor de gebruiker als schokkerig gedrag (jank). Daarom vertelt de React-documentatie je expliciet om te beginnen met useEffect en pas over te stappen naar useLayoutEffect wanneer je daadwerkelijk een flikkering ziet die je niet kunt tolereren.

Wanneer je elke hook gebruikt

De meeste van je logica hoort in useEffect. Gebruik het voor:

  • Het ophalen van data uit een API
  • Het instellen van subscriptions of event listeners
  • Het verzenden van analytics-events
  • Elke side effect die de layout niet onmiddellijk leest of muteert

Reserveer useLayoutEffect voor operaties die de DOM moeten lezen en terug moeten schrijven voordat de gebruiker het frame ziet:

  • Het meten van de afmetingen van elementen, zoals breedte, hoogte of scrollpositie
  • Het berekenen van coördinaten voor tooltips, popovers of contextmenu's
  • Het voorkomen van zichtbare layout-verschuivingen wanneer de visuele positie afhankelijk is van de gerenderde geometrie

Als je niet zeker weet welke je moet kiezen, kies dan standaard voor useEffect. Stap pas over naar useLayoutEffect wanneer je visuele instabiliteit opmerkt. Alleen al deze regel zal ervoor zorgen dat het overgrote deel van de React-applicaties soepel blijft draaien.

De Server-Side Rendering Valkuil

Als je Next.js, Remix of een ander framework gebruikt dat React op de server rendert, krijg je een waarschuwing bij useLayoutEffect. Omdat de server geen DOM heeft, heeft de hook niets om te meten. React waarschuwt je dat het een browseromgeving verwachtte en deze niet vond. Tijdens hydratatie kan deze mismatch ook subtiele bugs veroorzaken, omdat de op de server gerenderde markup en de eerste beoogde render van de client kunnen verschillen.

De standaardoplossing is een isomorfe hook die het juiste effect selecteert op basis van de omgeving:

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Gebruik deze wrapper in elke component die DOM-nodes moet meten, maar die mogelijk wordt uitgevoerd tijdens server-rendering. Het onderdrukt de waarschuwing en houdt je serveroutput consistent.

Performance en Best Practices

Omdat useLayoutEffect het tekenen (painting) blokkeert, moet je de body van de hook zo licht mogelijk houden. Lees de layout-waarde, bereken de correctie en schrijf deze terug. Haal geen gegevens op, parse geen grote objecten of voer geen zware algoritmen uit binnen de hook. Zware code hier zal de main thread vertragen en ervoor zorgen dat je interface bevroren aanvoelt.

Wanneer je elementen meet, gebruik dan React refs in plaats van document.getElementById. Refs zijn gekoppeld aan je component-instantie, overleven re-renders zonder query-trucs en werken betrouwbaar met portals of conditional rendering. Globale ID-lookups doorbreken de component-encapsulatie en kunnen null retourneren op precies het moment dat je ze nodig hebt.

useEffect is de juiste standaard voor bijna elke side effect. Het laat de browser zonder onderbreking tekenen en handelt data, events en externe synchronisatie netjes af. useLayoutEffect is een gespecialiseerd hulpmiddel voor een specifiek probleem: het lezen van de layout en het terugschrijven ervan vóór het tekenen. Beheers het tijdsverschil tussen beide, en je zult stoppen met het achterhalen van flikkeringen en ze in plaats daarvan gaan voorkomen.

De belangrijkste les: Begin met useEffect voor alles. Het moment dat je een tooltip of modal ziet flitsen op de verkeerde plek voordat deze zichzelf corrigeert, is dat je signaal. Schakel over naar useLayoutEffect, meet de DOM, pas je layout aan en laat de browser één keer tekenen — correct.