React

useEffect vs useLayoutEffect: quando cambia il risultato

useEffect gira dopo il paint del browser, useLayoutEffect gira prima e in modo sincrono: il secondo serve quando devi misurare il DOM prima che l’utente veda il frame.

In sintesi

  • useLayoutEffect è sincrono e blocca il paint: va usato solo quando la misura del DOM deve avvenire prima del frame visibile.
  • useEffect parte dopo il paint e resta la scelta giusta per dati, sottoscrizioni e log.
  • Sul server useLayoutEffect non può misurare nulla: React avvisa e la correzione arriva solo dopo l’idratazione.

La differenza è il momento: useEffect gira dopo che il browser ha dipinto, useLayoutEffect gira prima del paint e in modo sincrono. Se il risultato dipende da una misura del DOM vince il secondo, altrimenti il primo costa meno.

L’ordine dentro un frame

React aggiorna il DOM, poi esegue i layout effect, poi lascia dipingere il browser, poi esegue gli effect passivi. Il layout effect è sincrono: il paint aspetta che la funzione sia tornata.

  • Render: React calcola l’albero nuovo e lo confronta con il precedente.
  • Layout effect: gira subito dopo la scrittura sul DOM, prima che l’utente veda il frame.
  • Paint: il browser disegna quello che ha in pagina.
  • Effect: useEffect gira dopo, senza bloccare nulla.

Il caso in cui il risultato cambia

Un tooltip che deve centrarsi sull’ancora non conosce la propria larghezza finché non è nel DOM. Con useEffect il primo frame lo mostra spostato e il secondo lo corregge: l’occhio vede il salto. Con useLayoutEffect misura e correzione avvengono prima del paint.

function Tooltip({ anchor, children }) {
  const ref = useRef(null);
  const [left, setLeft] = useState(0);

  useLayoutEffect(() => {
    const width = ref.current.getBoundingClientRect().width;
    setLeft(anchor.x - width / 2);
  }, [anchor.x]);

  return (
    
      {children}
    
  );
}

Quando useEffect è la scelta giusta

Dati, sottoscrizioni, log e titolo della pagina non hanno bisogno di fermare il paint: un effect passivo lascia disegnare il browser e parte subito dopo. La regola pratica è partire da useEffect e passare all’altro solo quando una misura visibile lo richiede.

Il costo

Ogni layout effect allunga il frame: se legge e scrive il DOM in un ciclo, il browser aspetta. Un setState dentro useLayoutEffect provoca un render sincrono extra, e una lista lunga di questi componenti si sente nello scroll.

Provarlo in locale

npm create vite@latest effect-demo -- --template react-ts
cd effect-demo
npm install
npm run dev

Il template react-ts di Vite, con licenza MIT, include React 19 e TypeScript: apri src/App.tsx, sostituisci il contenuto con il componente qui sopra e osserva la pagina mentre la finestra cambia larghezza. Se lo stesso componente finisce in un render sul server, React stampa un avviso: sul server non esiste DOM da misurare.

Due record mostrano il problema in produzione: dropdown-menu, che sulle primitive Radix calcola la posizione prima del paint, e theme-toggle, che cambia icona senza mostrare il frame intermedio. Il resto è tra i 111.985 componenti e i 1.556 blocchi.

La limitazione

Su un componente renderizzato lato server useLayoutEffect non può misurare niente: l’HTML arriva senza la correzione e il valore giusto compare solo dopo l’idratazione. Per quel caso la misura va spostata in un effetto passivo o risolta in CSS.

Dove continuare

L’ordine delle chiamate è in regole degli hook; cosa cambia nella versione 19 è in React 19; il confronto fra framework sul costo del primo render è in framework frontend a confronto.

Domande frequenti

Perché useEffect non basta per misurare il DOM?

Perché parte dopo il paint: la misura arriva quando l’utente ha già visto il frame sbagliato. useLayoutEffect gira prima e permette di correggere lo stile senza mostrare il passaggio.

useLayoutEffect gira anche sul server?

No. Sul server non c’è layout da misurare, quindi React non lo esegue e riserva l’effetto all’idratazione, con un avviso in console durante il render sul server.

Quale usare per leggere localStorage?

useEffect, perché il valore non dipende dal layout. Il primo frame mostra il valore predefinito e il secondo quello salvato: se quel salto si vede, il rimedio è leggere il valore in un effetto di inizializzazione lato client.

useLayoutEffect può causare un loop infinito?

Sì, se dentro l’effetto aggiorni uno stato che compare nelle dipendenze. Il ciclo è sincrono, quindi blocca la pagina invece di rallentarla: React avvisa con “Maximum update depth exceeded”.

Come vedo la differenza in DevTools?

Nel pannello Performance, il tempo fra la scrittura del DOM e il primo paint include i layout effect. Confronta due registrazioni con lo stesso componente e i due hook per vedere quanto pesa la versione sincrona.