Hai mai costruito un tooltip che scatta dall'angolo in alto a sinistra alla sua posizione corretta? O una modal che appare con la dimensione sbagliata prima di assestarsi? Quel glitch di un istante è un flicker di layout. Accade quando React legge il DOM, calcola una correzione e aggiorna lo stato, ma il browser ha già iniziato a visualizzare i pixel sullo schermo. La soluzione abituale è sostituire useEffect con useLayoutEffect. Lo scambio funziona, ma solo se comprendi esattamente quando ogni hook viene attivato all'interno della pipeline del browser.

La pipeline del browser: Render, Commit, Paint

React aggiorna un componente in tre fasi distinte. Nella fase di render, React costruisce — o ricostruisce — il Virtual DOM e calcola la differenza (diff). Non ci sono ancora cambiamenti reali ai pixel; si tratta di puro calcolo che avviene in memoria. Segue la fase di commit, in cui React applica tali modifiche ai nodi del DOM reale. Gli stili si aggiornano, i nodi vengono inseriti o rimossi e il testo cambia.

A questo punto subentra il browser. Nella fase di paint, il motore di rendering del browser calcola la geometria del layout e disegna i pixel sullo schermo. Questa sequenza è rigida. Il browser deve completare il layout prima di poter eseguire il paint, e deve finire il paint prima che l'utente possa vedere qualsiasi novità. Il divario tra commit e paint si misura in millisecondi, ma è reale, ed è la finestra in cui useEffect e useLayoutEffect divergono.

Perché useEffect causa il flicker

useEffect viene eseguito in modo asincrono, programmato per attivarsi dopo che il browser ha già effettuato il paint dello schermo. Il DOM viene aggiornato, i pixel vengono disegnati e poi React interviene per eseguire il tuo effetto.

Immagina di renderizzare un menu a discesa sotto un pulsante. All'interno di useEffect, chiami buttonRef.current.getBoundingClientRect(), calcoli le coordinate corrette di top e left e le memorizzi nello stato. Poiché useEffect viene eseguito dopo il paint, il browser ha già disegnato il menu nella sua posizione predefinita, magari a top: 0, left: 0. Solo dopo quel paint il tuo effetto aggiorna lo stato. React esegue il commit delle coordinate corrette e il browser esegue nuovamente il paint. L'utente vede due frame: la posizione errata, poi quella corretta. Quello scatto visivo è il flicker che tutti cercano di evitare.

Per il fetching di dati, le chiamate API, il tracciamento analitico o la configurazione di event listener, questo ritardo non è rilevante. All'utente non importa se un segnale analitico viene inviato pochi millisecondi dopo il paint. Anzi, posticipare il lavoro non visivo dopo il paint mantiene il render iniziale reattivo. Ma per le correzioni dipendenti dal layout, useEffect è semplicemente troppo tardi.

Come useLayoutEffect blocca il paint

useLayoutEffect viene eseguito in modo sincrono, immediatamente dopo che React ha mutato il DOM ma prima che il browser abbia la possibilità di calcolare il layout o disegnare i pixel. Blocca completamente la pipeline di paint.

Se esegui la stessa misurazione del menu a discesa all'interno di useLayoutEffect, la sequenza cambia. React esegue il commit dell'aggiornamento iniziale del DOM, esegue il tuo layout effect e l'aggiornamento dello stato innesca un re-render sincrono. React esegue il commit delle coordinate corrette e solo allora il browser esegue il paint. L'utente vede un unico frame, già corretto.

Questo comportamento bloccante è sia un vantaggio che un rischio. Poiché useLayoutEffect impedisce al browser di eseguire il paint finché non ha terminato, qualsiasi calcolo pesante al suo interno congela l'interfaccia utente. Anche solo poche decine di millisecondi di paint bloccato sembrano un "jank" per l'utente. Ecco perché la documentazione di React ti consiglia esplicitamente di iniziare con useEffect e di passare a useLayoutEffect solo quando osservi effettivamente un flicker che non puoi tollerare.

Quando usare ciascun hook

La maggior parte della tua logica dovrebbe stare in useEffect. Usalo per:

  • Effettuare il fetching di dati da un'API
  • Configurare sottoscrizioni o event listener
  • Inviare eventi analitici
  • Qualsiasi side effect che non legga o muti il layout immediatamente

Riserva useLayoutEffect per operazioni che devono leggere il DOM e scrivere nuovamente prima che l'utente veda il frame:

  • Misurare le dimensioni di un elemento, come larghezza, altezza o posizione dello scroll
  • Calcolare le coordinate per tooltip, popover o menu contestuali
  • Prevenire spostamenti visibili del layout quando la posizione visiva dipende dalla geometria renderizzata

Se non sei sicuro di quale scegliere, usa di default useEffect. Passa a useLayoutEffect solo quando noti instabilità visiva. Questa sola regola manterrà la stragrande maggioranza delle applicazioni React fluida.

L'insidia del Server-Side Rendering

Se utilizzi Next.js, Remix o qualsiasi framework che esegua il rendering di React sul server, riceverai un avviso con useLayoutEffect. Poiché il server non ha un DOM, l'hook non ha nulla da misurare. React ti avvisa che si aspettava un ambiente browser e non lo ha trovato. Durante l'idratazione, questa discrepanza può anche causare bug sottili, poiché il markup renderizzato sul server e il primo rendering previsto sul client potrebbero differire.

La soluzione standard è un hook isomorfo che seleziona l'effetto corretto in base all'ambiente:

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

Usa questo wrapper in qualsiasi componente che debba misurare i nodi del DOM ma che potrebbe essere eseguito durante il rendering sul server. Silenzia l'avviso e mantiene coerente l'output del server.

Performance e Best Practice

Poiché useLayoutEffect blocca il painting, mantieni il corpo dell'hook il più leggero possibile. Leggi il valore del layout, calcola la correzione e riscrivila. Non recuperare dati, non analizzare oggetti di grandi dimensioni o eseguire algoritmi costosi al suo interno. Codice pesante in questa fase bloccherà il thread principale e farà sembrare l'interfaccia congelata.

Quando misuri gli elementi, usa le ref di React invece di document.getElementById. Le ref sono legate all'istanza del tuo componente, sopravvivono ai re-render senza trucchi di query e funzionano in modo affidabile con i portal o il rendering condizionale. La ricerca di ID globali rompe l'incapsulamento del componente e può restituire null proprio nel momento in cui ne hai bisogno.

useEffect è il default corretto per quasi ogni side effect. Permette al browser di effettuare il painting senza interruzioni e gestisce dati, eventi e sincronizzazione esterna in modo pulito. useLayoutEffect è uno strumento specializzato per un problema specifico: leggere il layout e riscriverlo prima del painting. Padroneggia la differenza di timing tra i due e smetterai di inseguire i flicker, iniziando a prevenirli.

Il punto fondamentale: Inizia con useEffect per tutto. Nel momento in cui vedi un tooltip o una modale lampeggiare nel posto sbagliato prima di correggersi, quello è il tuo segnale. Passa a useLayoutEffect, misura il DOM, regola il layout e lascia che il browser effettui il painting una volta sola — correttamente.