useCallback guarda en caché una función entre renderizados: devuelve el mismo objeto de función hasta que cambia una de sus dependencias. Eso solo importa cuando algo compara la función por referencia, que en la práctica significa un hijo envuelto en memo o un efecto que incluye la función como dependencia.
Ambos botones están memoizados y ambos hacen lo mismo. Cambia el tema y mira la consola: solo aparece render Inline function, porque saveInline es una función nueva en cada renderizado, así que memo ve una prop cambiada. saveCached es la misma función cada vez, así que ese botón se salta.
Lo que guarda en caché
const cachedFn = useCallback(fn, dependencies);
fnes la función que quieres conservar. React no la llama; te la devuelve.dependenciesenumera cada valor del componente que leefn. En cada renderizado React los compara con los anteriores usandoObject.is.- Si nada cambió, recibes la función del renderizado anterior. Si algo cambió, recibes la función que pasaste esta vez, y React la conserva para la próxima.
Fíjate en lo que esto significa: la función flecha dentro de useCallback se sigue creando en cada renderizado, porque es solo un argumento. useCallback no ahorra ese trabajo. Su único trabajo es devolverte una función anterior idéntica para que la referencia se mantenga igual.
useCallback vs useMemo
Son el mismo mecanismo. useMemo guarda en caché el resultado de llamar a una función, useCallback guarda en caché la función en sí:
// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);
Usa useCallback para funciones porque se lee mejor, y useMemo para valores calculados como una lista filtrada o un objeto de opciones.
Quitar dependencias con funciones de actualización
Una función en caché debe incluir cada valor de estado que lee, y cada vez que ese estado cambia, la función también cambia. En una lista de filas memoizadas eso es un problema: una función remove que lee todos cambia cada vez que cambia todos, así que cada fila restante vuelve a renderizar después de cada eliminación. Usa la forma de actualización del setter para que la función ya no lea todos en absoluto.
Quita una fila: la consola se queda en silencio, porque las demás filas recibieron el mismo objeto todo y la misma función remove que antes. Cambia la función a useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) y vuelve a probar: ahora cada fila restante registra un renderizado después de cada eliminación.
Funciones como dependencias de efectos
Un efecto que llama a una función declarada en el componente tiene que incluir esa función como dependencia. Si la función es nueva en cada renderizado, el efecto se ejecuta después de cada renderizado. useCallback la mantiene estable hasta que cambian sus propias entradas:
Escribir un mensaje no reconecta. Cambiar de sala registra una desconexión de la sala vieja y una conexión a la nueva. Borra el envoltorio useCallback (conserva la función flecha) y cada pulsación desconecta y vuelve a conectar.
Aun así, aquí hay una solución más simple: mueve la función dentro del efecto. Así el efecto depende directamente de roomId y ninguna función tiene que ser estable.
useEffect(() => {
function createOptions() {
return { serverUrl: 'wss://chat.example', roomId };
}
const options = createOptions();
// ...
}, [roomId]);
Prefiere eso cuando la función solo la usa el efecto. Deja useCallback para las funciones que también pasas hacia abajo o devuelves desde un hook personalizado, donde quienes las llaman podrían ponerlas en sus propios arrays de dependencias.
Comprobar si una función cambió
Cuando un hijo con memo sigue renderizando y no sabes por qué, comprueba si la prop de función es de verdad el mismo objeto entre renderizados. Guarda la anterior en una ref y compara:
Haz clic en Count: la consola dice increment changed: false, porque step no cambió. Haz clic en Step y dice true: step es una dependencia, así que React devolvió la función nueva. Una función que cambia en cada renderizado suele significar que una dependencia es a su vez nueva en cada renderizado, como un objeto u otra función declarada en el componente.
useCallback en hooks personalizados
Un hook personalizado que devuelve funciones es un buen lugar para useCallback aunque no sepas quién lo va a llamar. El componente que usa el hook puede pasar esas funciones a un hijo con memo o incluirlas en un efecto, y no puede hacerlas estables desde fuera. Envolverlas dentro del hook les da a todos los que lo llaman una referencia estable por defecto:
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 };
}
El error común: envolverlo todo
useCallback alrededor de un manejador que va directamente a un <button> o un <input> no sirve para nada. React no se ahorra ningún trabajo en un elemento del DOM porque su manejador siga siendo el mismo, y un componente que no está envuelto en memo renderiza igual cuando lo hace su padre.
// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;
Cada useCallback extra agrega un array de dependencias en el que alguien se puede equivocar, y una dependencia desactualizada es un error real (la función sigue leyendo estado viejo). Agrégalo cuando haya un hijo con memo o un array de dependencias del lado que lo recibe, no por defecto.
El React Compiler 1.0, cuando tu build lo usa, memoiza funciones y valores automáticamente, lo que elimina la necesidad de la mayoría de las llamadas a useCallback escritas a mano. Sin el compilador, las reglas de arriba siguen aplicándose, y useCallback manual sigue siendo habitual en el código existente.
Preguntas frecuentes
¿Qué hace useCallback?
Devuelve el mismo objeto de función en cada renderizado hasta que cambia una de sus dependencias. No evita que se cree la función; devuelve la función de un renderizado anterior en lugar de la nueva.
¿Cuándo debo usar useCallback?
Cuando pasas una función a un hijo envuelto en memo, o cuando la función es una dependencia de un efecto o de otro hook. En ambos casos, una función nueva en cada renderizado anularía la optimización o volvería a ejecutar el efecto.
¿Cuál es la diferencia entre useCallback y useMemo?
useCallback(fn, deps) guarda en caché la propia fn. useMemo(() => value, deps) guarda en caché el valor que devuelve una función. useCallback(fn, deps) es exactamente useMemo(() => fn, deps).
¿Debo envolver todas las funciones en useCallback?
No. Si la función va a un elemento simple del DOM o a un componente que no está memoizado, una referencia estable no cambia nada, y los arrays de dependencias extra hacen el código más difícil de leer y más fácil de equivocar.
¿useCallback hace más rápido mi componente?
No por sí solo. Solo ayuda cuando algo más abajo compara la función por referencia, como memo o el array de dependencias de un efecto.