useMemo mette in cache il risultato di un calcolo tra un rendering e l'altro. React esegue la tua funzione al primo rendering, conserva il risultato e restituisce lo stesso risultato nei rendering successivi finché uno dei valori dell'array delle dipendenze non cambia.
Apri la Console e cambia il tema qualche volta: non viene registrato nulla, e il cambio sembra istantaneo. Cambia scheda e compare filtering for active, con una breve pausa, perché tab è una dipendenza. Ora sostituisci la riga con useMemo con const visible = filterTodos(todos, tab); e cambia di nuovo il tema: ogni clic registra un log e si blocca per 100 ms.
La sintassi
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValueè una funzione senza argomenti che restituisce il valore che vuoi mettere in cache. React la chiama durante il rendering.dependenciesè la lista di ogni valore del componente che la funzione legge: props, stato e variabili dichiarate nel corpo del componente.- A ogni rendering React confronta ogni dipendenza con il suo valore del rendering precedente usando
Object.is. Se coincidono tutte, restituisce il valore in cache senza chiamare la tua funzione. Se una qualsiasi è diversa, chiama la funzione e mette in cache il nuovo risultato.
useMemo è un hook, quindi segue le regole degli hook: chiamalo al livello più alto del componente, non dentro un ciclo o una condizione.
Le dipendenze devono essere complete
Togli un valore dall'array e la cache continua a fornire un risultato calcolato dal suo vecchio valore. Qui il totale legge qty ma elenca solo price:
Clicca Quantity: passa a 2, ma il totale resta a 10 e non viene registrato nulla. Clicca Price e il totale salta a 22, perché solo allora il calcolo viene eseguito di nuovo, leggendo ora il qty attuale. Aggiungi qty all'array ed entrambi i pulsanti aggiornano il totale. In un progetto reale la regola di lint react-hooks/exhaustive-deps intercetta questo errore.
L'errore opposto è una dipendenza che è nuova a ogni rendering, come un oggetto creato nel corpo del componente. Non coincide mai con il suo valore precedente, quindi la cache non viene mai usata:
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
Mantenere stabile un oggetto per un figlio con memo
Il secondo motivo per usare useMemo non ha nulla a che fare con i calcoli lenti. Un figlio racchiuso in memo salta il rendering solo quando ogni prop è uguale all'ultima volta. Un letterale di oggetto scritto nel corpo del componente è un nuovo oggetto a ogni rendering, quindi vanifica memo anche quando il suo contenuto non è cambiato. useMemo mantiene lo stesso oggetto finché i suoi input non cambiano.
Scrivi nel campo della nota e Chart resta in silenzio, mentre Switch unit registra un rendering. Sostituisci la chiamata a useMemo con un semplice const options = { unit, max: unit === 'km' ? 100 : 62 }; e ogni tasto nella nota registra render Chart.
Lo stesso vale per un effetto che dipende da un oggetto. Se un effetto elenca options tra le sue dipendenze e options viene ricostruito a ogni rendering, l'effetto viene eseguito dopo ogni rendering. useMemo lo risolve, ma spesso la soluzione più semplice è creare l'oggetto dentro l'effetto e dipendere dai valori primitivi:
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
Quando non usare useMemo
La maggior parte dei calcoli in un componente è leggera: sommare numeri, unire due stringhe, filtrare qualche decina di voci. Metterli in cache non fa risparmiare quasi nulla: React memorizza comunque il valore e confronta le dipendenze a ogni rendering, e il codice diventa più difficile da leggere.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
Per scoprire se qualcosa è lento, misuralo. Racchiudi il calcolo tra console.time('filter') e console.timeEnd('filter'), esegui l'interazione e guarda il numero. Come indicazione di massima, un lavoro che richiede un millisecondo o più sulla tua macchina è un candidato, tenendo presente che i dispositivi dei tuoi utenti potrebbero essere più lenti. Prima di ricorrere a useMemo, verifica anche se il lavoro deve davvero stare nel rendering: i dati che non cambiano mai possono essere calcolati una volta fuori dal componente, e uno stato usato solo da una piccola parte della pagina può spostarsi in un componente più piccolo (vedi rendering).
useMemo vs useEffect per i valori derivati
Uno schema comune è calcolare un valore in un effetto e memorizzarlo nello stato:
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
La versione con effetto si renderizza una volta con il vecchio visible, poi esegue l'effetto, imposta lo stato e si renderizza di nuovo. È un rendering in più, e nel primo la lista non corrisponde alla scheda. Calcolare durante il rendering con useMemo (o senza alcun hook, se il lavoro è leggero) dà il valore giusto nello stesso rendering. Gli effetti servono a sincronizzarsi con cose al di fuori di React, non a derivare un pezzo di stato da un altro.
Un suggerimento per le prestazioni, non una garanzia
useMemo dice a React che può riutilizzare un risultato. Non promette che lo farà. React mantiene il valore in cache durante il normale funzionamento, ma può buttarlo via, per esempio quando modifichi il file durante lo sviluppo, o quando un componente si sospende durante il suo primo montaggio. Scrivi il codice in modo che sia corretto anche se il calcolo viene eseguito di nuovo: la funzione deve essere pura, senza effetti collaterali come richieste, sottoscrizioni o modifiche a variabili esterne. Gli effetti collaterali vanno in un gestore di eventi o in un effetto.
In sviluppo, con StrictMode, React chiama la tua funzione di calcolo due volte per aiutarti a individuare il codice impuro. L'anteprima qui gira come una build di produzione, quindi vedrai ogni log filtering for una sola volta.
useMemo e React Compiler
React Compiler 1.0 è uno strumento di build che analizza i tuoi componenti e aggiunge la memoizzazione automaticamente, mettendo in cache calcoli e JSX nei punti in cui useMemo, useCallback e memo sarebbero utili. In un progetto che lo usa, ne scriverai meno a mano. useMemo scritto a mano funziona ancora accanto al compilatore, resta lo strumento da usare quando il compilatore non è configurato, e lo incontrerai nella maggior parte del codice React esistente.
Domande frequenti
Cosa fa useMemo in React?
Chiama la tua funzione di calcolo al primo rendering, memorizza il risultato e, nei rendering successivi, restituisce il risultato memorizzato finché ogni valore dell'array delle dipendenze è uguale all'ultima volta (confrontato con Object.is).
Quando dovrei usare useMemo?
Quando un calcolo è sensibilmente lento e i suoi input cambiano di rado, oppure quando ti serve che un oggetto o un array mantenga lo stesso riferimento tra un rendering e l'altro perché un figlio con memo o un effetto dipendono da esso. Per un lavoro leggero, lascialo stare.
Qual è la differenza tra useMemo e useCallback?
useMemo mette in cache il valore che la tua funzione restituisce. useCallback mette in cache la funzione stessa. useCallback(fn, deps) equivale a useMemo(() => fn, deps).
useMemo garantisce che il valore non venga mai ricalcolato?
No. È un suggerimento per le prestazioni. React può buttare via il valore in cache in alcuni casi, per esempio in sviluppo quando modifichi il file, quindi il tuo codice deve funzionare correttamente anche se il calcolo viene eseguito di nuovo.
Con React Compiler mi serve ancora useMemo?
React Compiler può aggiungere la memoizzazione automaticamente quando la tua build lo usa. Senza il compilatore, o nei casi che salta, useMemo scritto a mano funziona ancora allo stesso modo ed è ancora comune nel codice esistente.