Un componente di chat riconnette il suo WebSocket ogni volta che scrivi una lettera in una casella di ricerca che non ha niente a che fare con la chat. Un altro componente si ri-renderizza in loop finché la tab si blocca. Un terzo registra il suo useEffect due volte al montaggio, ma solo in sviluppo. Nessuno di questi è un bug di React. I primi due sono l’array delle dipendenze che fa esattamente quello che gli hai detto di fare, e il terzo è la Strict Mode che verifica che il tuo effetto sappia pulire dopo di sé.
Quando usare useEffect in un componente React
useEffect sincronizza un componente con qualcosa che vive fuori da React: una connessione a un server, un’API del browser, un timer, il DOM direttamente, un widget di terze parti. React renderizza il tuo JSX, lo applica al DOM, e solo dopo esegue i tuoi effetti. Quando il render non è stato causato da un’interazione come un click, React di solito lascia che il browser disegni prima la nuova schermata.
Quindi un effetto è il posto sbagliato per calcolare un valore destinato al render. Se fullName si può derivare da firstName e lastName, scrivilo durante il render:
| |
Non un effetto che chiama setFullName a posteriori. La versione con l’effetto renderizza una volta con il valore vecchio, pianifica un aggiornamento di stato, poi renderizza di nuovo. Quel passaggio extra non porta niente. Usa useEffect quando il componente deve fare qualcosa perché è stato mostrato, non per calcolare qualcosa che potrebbe calcolare direttamente.
Come funziona l’array delle dipendenze di useEffect
Il secondo argomento di useEffect si presenta in tre forme, e ciascuna dice a React qualcosa di diverso su quando ri-eseguire la funzione.
| |
Per un array popolato, React confronta ogni elemento con il suo valore al render precedente usando Object.is. È lo stesso controllo di ===, tranne che NaN è uguale a se stesso e 0 e -0 sono diversi. Se ogni dipendenza è identica all’ultima volta, React salta l’effetto. Se una è diversa, lo esegue di nuovo. Non esiste un confronto parziale, né un modo per dire a React “ri-esegui solo quando è cambiata proprio questa dipendenza”. Quella logica deve stare dentro l’effetto.
Significa anche che non puoi omettere un valore dall’array per impedire che l’effetto si ri-esegua. L’array dovrebbe elencare ogni valore reattivo che il corpo della funzione legge: state, props, e qualsiasi cosa derivata da loro. Togli un valore dall’array ma continua a leggerlo, e l’effetto gira con una copia vecchia. La regola react-hooks/exhaustive-deps di eslint-plugin-react-hooks esiste apposta per catturare questo tipo di disallineamento. Trattala come un errore di build, non silenziarla con un commento di disattivazione.
Cosa fa la funzione di cleanup di useEffect
Un effetto può restituire una funzione. React tratta quel valore di ritorno come il cleanup di qualunque cosa l’effetto abbia avviato:
| |
React chiama il cleanup in due situazioni: appena prima che l’effetto si ri-esegua perché una dipendenza è cambiata, e quando il componente viene smontato. Pulisce sempre l’effetto precedente prima di avviare il successivo. Connetti, disconnetti, connetti, disconnetti. Mai due connessioni aperte insieme.
Qualunque cosa un effetto avvii e che richieda uno stop esplicito va dietro una funzione di cleanup: subscribe richiede unsubscribe, addEventListener richiede removeEventListener, setInterval richiede clearInterval. Lo stesso schema risolve un bug più sottile nel fetching dei dati, dove una risposta vecchia arriva dopo una più recente:
| |
Se personId cambia prima che la prima richiesta si risolva, il cleanup imposta ignore a true per quella richiesta, quindi il suo callback .then() non fa nulla invece di sovrascrivere uno stato più recente con dati più vecchi.
Perché useEffect viene eseguito due volte in sviluppo
In sviluppo, sotto StrictMode, React esegue il setup, poi il cleanup, poi di nuovo il setup, immediatamente dopo il primo montaggio. In produzione il setup viene eseguito una volta sola. React lo fa apposta: il tuo componente verrà smontato e rimontato nell’uso reale (per esempio quando navighi via da una rotta e poi torni), e la Strict Mode forza questo a succedere prima sulla tua macchina, dove un cleanup mancante si vede come un bug visibile.
L’errore è correggere il sintomo:
| |
Il guard blocca la seconda chiamata a connect(), ma manca comunque il cleanup, quindi la connessione resta aperta anche dopo che il componente viene smontato. Se il remount rompe un effetto, all’effetto manca il cleanup. Scrivi la funzione di cleanup invece di difenderti dalla seconda esecuzione.
Errori comuni con le dipendenze di useEffect
Un oggetto o una funzione letterale ricreati a ogni render. Due oggetti con lo stesso contenuto non sono lo stesso oggetto:
| |
Metti un oggetto così in un array delle dipendenze e l’effetto vede un valore “nuovo” a ogni render, che sia cambiato qualcosa a cui tieni o no:
| |
Costruisci l’oggetto dentro l’effetto, invece, e dipendi dai valori primitivi da cui è fatto:
| |
Leggere lo state solo per calcolare il prossimo state. Un interval che legge count per incrementarlo costringe l’effetto a dipendere da count, il che ricrea l’interval a ogni tick:
| |
Un aggiornamento funzionale rimuove la lettura, e la dipendenza se ne va con essa:
| |
Un effetto che fa due lavori non collegati. Immagina un effetto che recupera le città di un paese e le zone di una città. Con entrambe nella stessa funzione e lo stesso array di dipendenze, cambiare la città rifà il fetch anche delle città. Separali: un effetto per ogni pezzo indipendente di sincronizzazione, ciascuno con il proprio array.
Quando non serve useEffect
Due schemi sembrano richiedere un effetto e non lo richiedono.
Azzerare lo state quando una prop cambia. Svuotare lo state locale di un form quando userId cambia è tentante da scrivere come un effetto che osserva userId. Funziona, ma il componente renderizza una volta con lo state vecchio prima che il reset arrivi. Passa invece il valore che cambia come key del componente, e React lo tratta come un’istanza nuova, senza nessun effetto di mezzo:
| |
Qualcosa che succede per un click, non per un render. Una notifica “aggiunto al carrello” che osserva product.isInCart in un effetto scatta di nuovo a ogni reload della pagina dove l’articolo è già nel carrello, perché l’effetto non riesce a distinguere “appena aggiunto” da “già presente”. La notifica appartiene all’handler del click che aggiunge l’articolo, dove viene eseguita solo quando l’utente clicca:
| |
Se qualcosa deve succedere perché l’utente ha fatto qualcosa, metti quella logica nell’event handler di quel qualcosa. useEffect serve per ciò che deve restare sincronizzato per tutto il tempo che il componente è a schermo, indipendentemente da cosa fa l’utente. Sincronizzare una fetch con il ciclo di vita del componente è quel tipo di problema (il lato della richiesta è trattato in gestione degli errori della Fetch API), e lo stesso vale per disconnettere un observer al momento dello smontaggio, come con l’Intersection Observer API.
La prossima volta che un effetto si comporta male, lascia l’array in pace all’inizio. Elenca cosa legge il corpo della funzione, decidi se ogni lettura deve davvero essere reattiva, e verifica se il codice funziona senza nessun effetto. Correggi il corpo, e l’array giusto segue da lì. Se è l’ordine dei callback asincroni intorno a un render che ti confonde, leggi come funzionano async/await e l’event loop, che tratta lo stack di chiamata e la coda dei microtask.