React

useCallback: l’uso che paga e quello che è rumore

useCallback mantiene stabile l’identità di una funzione fra due render. Paga quando quella funzione è una dipendenza in un effect o una prop di un componente memo.

In sintesi

  • useCallback non rende più veloce la funzione: cambia solo il riferimento fra un render e l’altro.
  • Il guadagno arriva quando l’identità è una dipendenza di un effect o una prop di un componente memo.
  • La regola exhaustive-deps resta un avviso: a volte la correzione è togliere il useCallback.

useCallback restituisce la stessa funzione fra due render finché le dipendenze non cambiano. Conviene quando quell’identità è una dipendenza in un effect o una prop di un componente memo; altrove è rumore.

Cosa cambia e cosa no

La funzione non diventa più veloce: cambia solo l’identità. React confronta il riferimento con quello del render precedente e riusa il vecchio se le dipendenze sono uguali. Il corpo viene rieseguito a ogni chiamata, come prima.

I tre casi in cui paga

  • Dipendenza di un effect: una funzione stabile evita di ri-sottoscrivere o riavviare un timer a ogni render.
  • Prop di un componente memo: senza identità stabile il confronto fallisce e il memo non produce nulla.
  • Dipendenza di un altro hook: useMemo e useCallback si concatenano, e un’identità instabile propaga il ricalcolo.

Il caso reale: ricerca con debounce

function Search({ onQuery }) {
  const [input, setInput] = useState("");

  const commit = useCallback((value) => onQuery(value), [onQuery]);

  useEffect(() => {
    const id = setTimeout(() => commit(input), 250);
    return () => clearTimeout(id);
  }, [input, commit]);

  return (
     setInput(event.target.value)} />
  );
}

Senza useCallback, commit sarebbe una funzione nuova a ogni render e l’effect si riavvierebbe a ogni tasto, azzerando il timer prima che scada. Con la dipendenza stabile il timer parte una volta per valore digitato.

Il rumore

Avvolgere ogni handler in useCallback con dipendenze vuote aggiunge una lista da mantenere e nasconde le variabili che la funzione legge: se il corpo usa input senza dichiararlo, il valore resta quello del primo render. Il guadagno è nullo finché nessuno confronta quell’identità.

Verificare le dipendenze

npm install --save-dev eslint eslint-plugin-react-hooks
npx eslint src

La regola react-hooks/exhaustive-deps del plugin, con licenza MIT, elenca le dipendenze mancanti; è un avviso e non una correzione automatica. A volte la risposta giusta è togliere il useCallback, non aggiungere la dipendenza.

Un caso concreto è filters, dove i gestori di cambio passano a componenti figli, e expandable-tabs, dove la callback di cambio tab attraversa un componente memo. Il resto sta tra i 111.985 componenti e i 1.556 blocchi.

La limitazione

useCallback non riduce il lavoro del browser: se il figlio non è memoizzato e l’identità non finisce in una lista di dipendenze, la funzione viene ricreata e confrontata senza alcun vantaggio. Il costo è una lista di dipendenze in più da tenere corretta a mano.

Dove continuare

Il componente che beneficia della funzione stabile è in React.memo; le regole che il linter controlla sono in regole degli hook; il confronto con l’altro framework diffuso è in Vue 3 vs React.

Domande frequenti

useCallback e useMemo: quale usare?

useMemo conserva un valore calcolato, useCallback conserva una funzione. Se il risultato è una funzione, useCallback è la forma abbreviata di useMemo con una funzione dentro.

Posso usare useCallback senza dipendenze?

Sì, con un array vuoto la funzione resta la stessa per tutta la vita del componente. È corretto solo se il corpo non legge props o stato: altrimenti cattura i valori del primo render e resta fermo.

useCallback rende il codice più veloce?

No. Il corpo viene eseguito a ogni chiamata come prima; cambia solo il riferimento della funzione. La velocità arriva dal componente che smette di ri-renderizzarsi perché l’identità è stabile.

Serve useCallback con il compilatore di React?

Molto meno. Il compilatore applica la memoizzazione automatica e conserva le identità dove serve, quindi l’uso manuale resta per i casi che una misura ha dimostrato lenti.

Perché la mia funzione cambia ancora a ogni render?

Perché una dipendenza nella lista è instabile: un oggetto creato nel render, una prop che cambia o un’altra funzione non memoizzata. Finché quella dipendenza cambia, useCallback restituisce una funzione nuova.