useCallback mette in cache una funzione tra un rendering e l'altro: restituisce lo stesso oggetto funzione finché una delle sue dipendenze non cambia. Questo conta solo quando qualcosa confronta la funzione per riferimento, il che in pratica significa un figlio racchiuso in memo o un effetto che elenca la funzione come dipendenza.
Entrambi i pulsanti sono memorizzati ed entrambi fanno la stessa cosa. Cambia il tema e guarda la Console: compare solo render Inline function, perché saveInline è una nuova funzione a ogni rendering, quindi memo vede una prop cambiata. saveCached è ogni volta la stessa funzione, quindi quel pulsante viene saltato.
Cosa mette in cache
const cachedFn = useCallback(fn, dependencies);
fnè la funzione che vuoi conservare. React non la chiama; te la restituisce.dependencieselenca ogni valore del componente chefnlegge. A ogni rendering React li confronta con quelli precedenti usandoObject.is.- Se non è cambiato nulla, ricevi la funzione del rendering precedente. Se qualcosa è cambiato, ricevi la funzione che hai passato questa volta, e React conserva quella per la volta successiva.
Nota cosa significa: la arrow function dentro useCallback viene comunque creata a ogni rendering, perché è solo un argomento. useCallback non risparmia quel lavoro. Il suo unico compito è restituire una funzione più vecchia e identica così che il riferimento resti lo stesso.
useCallback vs useMemo
Sono lo stesso meccanismo. useMemo mette in cache il risultato della chiamata a una funzione, useCallback mette in cache la funzione stessa:
// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);
Usa useCallback per le funzioni perché si legge meglio, e useMemo per i valori calcolati come una lista filtrata o un oggetto di opzioni.
Eliminare dipendenze con le funzioni di aggiornamento
Una funzione in cache deve elencare ogni valore di stato che legge, e ogni volta che quello stato cambia, cambia anche la funzione. Per una lista di righe memorizzate questo è un problema: una funzione remove che legge todos cambia ogni volta che cambia todos, quindi ogni riga rimasta esegue un nuovo render dopo ogni rimozione. Usa la forma con funzione di aggiornamento del setter così che la funzione non legga più todos affatto.
Rimuovi una riga: la Console resta in silenzio, perché le altre righe hanno ricevuto lo stesso oggetto todo e la stessa funzione remove di prima. Cambia la funzione in useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) e riprova: ora ogni riga rimasta registra un rendering dopo ogni rimozione.
Funzioni come dipendenze degli effetti
Un effetto che chiama una funzione dichiarata nel componente deve elencare quella funzione come dipendenza. Se la funzione è nuova a ogni rendering, l'effetto viene eseguito dopo ogni rendering. useCallback la mantiene stabile finché i suoi stessi input non cambiano:
Scrivere un messaggio non provoca una nuova connessione. Cambiare stanza registra una disconnessione dalla vecchia stanza e una connessione alla nuova. Elimina il wrapper useCallback (tieni la arrow function) e ogni tasto disconnette e riconnette.
C'è però una soluzione più semplice: sposta la funzione dentro l'effetto. Allora l'effetto dipende direttamente da roomId e nessuna funzione deve essere stabile.
useEffect(() => {
function createOptions() {
return { serverUrl: 'wss://chat.example', roomId };
}
const options = createOptions();
// ...
}, [roomId]);
Preferiscila quando la funzione è usata solo dall'effetto. Tieni useCallback per le funzioni che passi anche verso il basso o che restituisci da un hook personalizzato, dove chi le usa potrebbe metterle nei propri array delle dipendenze.
Verificare se una funzione è cambiata
Quando un figlio con memo continua a renderizzarsi e non sai perché, verifica se la prop funzione è davvero lo stesso oggetto tra un rendering e l'altro. Tieni quella precedente in un ref e confrontale:
Clicca Count: la Console dice increment changed: false, perché step non è cambiato. Clicca Step e dice true: step è una dipendenza, quindi React ha restituito la nuova funzione. Una funzione che cambia a ogni rendering di solito significa che una dipendenza è a sua volta nuova a ogni rendering, come un oggetto o un'altra funzione dichiarata nel componente.
useCallback negli hook personalizzati
Un hook personalizzato che restituisce funzioni è un buon posto per useCallback anche quando non sai chi lo chiamerà. Il componente che usa l'hook potrebbe passare quelle funzioni a un figlio con memo o elencarle in un effetto, e non può renderle stabili dall'esterno. Racchiuderle dentro l'hook dà a ogni chiamante un riferimento stabile per default:
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, increment, reset };
}
L'errore comune: racchiudere tutto
useCallback attorno a un gestore che va direttamente a un <button> o a un <input> non fa nulla di utile. React non salta alcun lavoro per un elemento DOM perché il suo gestore è rimasto lo stesso, e un componente non racchiuso in memo si renderizza comunque quando lo fa il suo genitore.
// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;
Ogni useCallback in più aggiunge un array delle dipendenze che qualcuno può sbagliare, e una dipendenza vecchia è un vero bug (la funzione continua a leggere lo stato vecchio). Aggiungilo quando dall'altra parte c'è un figlio con memo o un array delle dipendenze, non per default.
React Compiler 1.0, quando la tua build lo usa, memorizza funzioni e valori automaticamente, il che elimina la necessità della maggior parte delle chiamate a useCallback scritte a mano. Senza il compilatore, le regole qui sopra valgono ancora, e useCallback scritto a mano è ancora comune nel codice esistente.
Domande frequenti
Cosa fa useCallback?
Restituisce lo stesso oggetto funzione a ogni rendering finché una delle sue dipendenze non cambia. Non impedisce che la funzione venga creata; restituisce la funzione di un rendering precedente invece di quella nuova.
Quando dovrei usare useCallback?
Quando passi una funzione a un figlio racchiuso in memo, o quando la funzione è una dipendenza di un effetto o di un altro hook. In entrambi i casi una nuova funzione a ogni rendering vanificherebbe l'ottimizzazione o eseguirebbe di nuovo l'effetto.
Qual è la differenza tra useCallback e useMemo?
useCallback(fn, deps) mette in cache fn stessa. useMemo(() => value, deps) mette in cache il valore che una funzione restituisce. useCallback(fn, deps) equivale esattamente a useMemo(() => fn, deps).
Dovrei racchiudere ogni funzione in useCallback?
No. Se la funzione va a un semplice elemento DOM o a un componente non memorizzato, un riferimento stabile non cambia nulla, e gli array delle dipendenze in più rendono il codice più difficile da leggere e più facile da sbagliare.
useCallback rende il mio componente più veloce?
Non da solo. Serve solo quando qualcosa più in basso confronta la funzione per riferimento, come memo o l'array delle dipendenze di un effetto.