useCallback es un Hook de React que te permite almacenar la definición de una función entre renderizados subsecuentes.

const cachedFn = useCallback(fn, dependencies)

Referencia

useCallback(fn, dependencias)

Llama a useCallback en el nivel superior de tu componente guardar en caché entre rerenderizados una definición de una función:

import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);

Observa más ejemplos debajo.

Parámetros

  • fn: La función que deseas guardar en caché. Puede recibir cualquier argumento y devolver cualquier valor. React retornará (¡no llamará!) tu función durante el renderizado inicial. En los renderizados subsecuentes, React retornará la misma función nuevamente si las dependencias no han cambiado desde el último renderizado. Si no es así, React retornará la función que pasaste durante el renderizado actual, y la almacenará en caso de que se necesite reutilizar más adelante. React no llamará a la función. La función se retornará para que puedas decidir si y cuándo llamarla.

  • dependencias: La lista de todos los valores reactivos dentro de la función fn. Los valores reactivos incluyen props, estado y todas las variables y funciones declaradas directamente dentro del cuerpo de tu componente. Si tu linter está configurado para React, verificará que cada valor reactivo esté debidamente especificado como una dependencia. La lista de dependencias debe tener un número constante de elementos y estar escrita en línea, de la forma [dep1, dep2, dep3]. React comparará cada dependencia con su valor anterior usando el algoritmo de comparación Object.is.

Devuelve

En el renderizado inicial, useCallback devuelve la función fn que le has enviado.

Durante los renderizados siguientes, puede devolver una función fn ya almacenada desde el último renderizado (si las dependencias no han cambiado), o devolver la función fn que hayas enviado durante el renderizado actual.

Advertencias

  • useCallback es un Hook, por lo que solo puedes llamarlo en el nivel superior de tu componente o en tus propios Hooks. No puedes llamarlo dentro de un ciclo ni de una condición. Si necesitas hacerlo, debes extraer un nuevo componente y mover el estado a él.
  • React no descartará la función almacenada a menos que haya una razón específica para hacerlo. Por ejemplo, en el ambiente de desarrollo, React descarta la caché cuando editas algún archivo de tu componente. Tanto en desarrollo como en producción, React descartará la caché si tu componente se suspende durante el montaje inicial. En el futuro, es posible que React agregue más características que aprovechen el descarte de caché —por ejemplo, si React agrega soporte nativo para listas virtuales en el futuro, tendría sentido descartar la caché para los elementos que estén fuera de la vista de la tabla virtualizada. Esto debería cumplir con tus expectativas si dependes de useCallback como una optimización de rendimiento. De lo contrario, una variable de estado o una referencia podrían ser más apropiadas.

Uso

Omitir rerenderizados de componentes

Cuando optimizas el rendimiento del renderizado, a veces necesitarás almacenar en caché las funciones que pasas a los componentes hijos. Veamos primero la sintaxis para hacerlo y luego veamos en qué casos es útil.

Para almacenar una función entre rerenderizados de tu componente, envuelve su definición en el Hook useCallback:

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...

Debes enviar dos elementos a useCallback:

  1. La definición de la función que quieres almacenar en caché entre rerenderizados.
  2. Una lista de dependencias que incluya cada valor dentro de tu componente que se usa dentro de tu función.

En el primer renderizado, la función retornada por useCallback será la función que pasaste.

En los siguientes renderizados, React comparará las dependencias con aquellas que pasaste en el renderizado anterior. Si ninguna de las dependencias ha cambiado (comparadas con Object.is), useCallback retornará la misma función que antes. De lo contrario, useCallback retornará la función que pasaste en este renderizado.

En otras palabras, useCallback almacena una función entre renderizados subsecuentes hasta que sus dependencias cambien.

Vamos a ver un ejemplo para entender cuándo esto es útil.

Supongamos que estás pasando una función handleSubmit desde ProductPage hacia el componente ShippingForm:

function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);

Notarás que cambiar la propiedad theme congela la aplicación por un momento, pero si pruebas eliminar <ShippingForm /> de tu JSX, se siente rápido. Esto te dice que vale la pena intentar optimizar el componente ShippingForm.

Por defecto, cuando un componente se renderiza nuevamente, React renderiza recursivamente a todos sus hijos. Esto es porque, cuando ProductPage se renderiza nuevamente con un theme diferente, el componente ShippingForm también se renderiza nuevamente. Esto está bien para componentes que no requieren mucho cálculo para renderizarse nuevamente. Pero si has verificado que un renderizado es lento, puedes decirle a ShippingForm que omita el rerenderizado cuando sus props son las mismas que en el último renderizado, envolviéndolo en memo:

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});

Con este cambio, ShippingForm omitirá el rerenderizado si todas las props son las mismas que en el último renderizado. Acá es donde el almacenamiento en caché de una función se vuelve importante. Imagina que definiste handleSubmit sin useCallback:

function ProductPage({ productId, referrer, theme }) {
// Cada vez que el tema cambie, esta será una función diferente...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}

return (
<div className={theme}>
{/* ... así las props de ShippingForm nunca serán iguales, y cada vez se renderizará nuevamente */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}

En JavaScript, la expresión function () {} o () => {} siempre crea una función diferente, similar a como el objeto literal {} siempre crea un nuevo objeto. Normalmente, esto no sería un problema, pero en este caso significa que las props de ShippingForm nunca serán las mismas, y tu optimización con memo no funcionará. Aquí es donde useCallback se vuelve útil:

function ProductPage({ productId, referrer, theme }) {
// Dile a React que almacene tu función entre rerenderizados...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...siempre y cuando estas dependencias no cambien...

return (
<div className={theme}>
{/* ...ShippingForm recibirá las mismas props y omitirá el rerenderizado */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}

Al envolver handleSubmit en useCallback, te aseguras de que sea la misma función entre los renderizados subsecuentes (hasta que las dependencias cambien). No deberías envolver una función en useCallback a menos de que lo hagas por alguna razón específica. En este ejemplo, la razón por la que pasamos handleSubmit a un componente envuelto en memo es que esto le permite omitir el rerenderizado. Existen otras razones por las que podrías necesitar useCallback que se describen más adelante en esta página.

Nota

Solo deberías pensar en useCallback como en una optimización de rendimiento. Si tu código no funciona sin él, encuentra el problema subyacente y arréglalo primero. Luego puedes agregar useCallback para mejorar el rendimiento.

Deep Dive

Ocasionalmente verás useMemo junto a useCallback. Ambos son útiles cuando deseas optimizar un componente hijo. Te permiten memoizar (o, en otras palabras, guardar en caché) aquello que estás enviando:

import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);

const requirements = useMemo(() => { // Llama a la función y almacena su resultado
return computeRequirements(product);
}, [product]);

const handleSubmit = useCallback((orderDetails) => { // Almacena la función como tal
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);

return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}

La diferencia está en qué te permiten almacenar:

  • useMemo almacena el resultado de tu función. En este ejemplo, se almacena el resultado de computeRequirements(product) para que no cambie a menos que product cambie. Esto permite enviar el objeto requirements sin rerenderizar ShippingForm innecesariamente. Cuando realmente sea necesario, React llamará a la función durante el renderizado para calcular su resultado.
  • useCallback almacena la función en sí. A diferencia de useMemo, no llama a la función recibida. En su lugar, almacena la función que proporcionaste para que handleSubmit en sí no cambie a menos que productId o referrer cambien. Esto permite enviar la función handleSubmit sin rerenderizar ShippingForm innecesariamente. Tu código no se llamará hasta que el usuario envíe el formulario.

Si ya estás familiarizado con useMemo, tal vez te sea útil ver useCallback como esto:

// Implementación simplificada (dentro de React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}

Lee más sobre la diferencia entre useMemo y useCallback.

Deep Dive

¿Deberías siempre usar useCallback?

Si tu aplicación es similar a este sitio, y la mayoría de las interacciones son bastas (como reemplazar una página o una sección entera), la memoización generalmente es innecesaria. Por otro lado, si tu aplicación es similar a un editor de dibujo, y la mayor parte de sus interacciones son granulares (como mover figuras), entonces la memoización puede ser muy útil.

Almacenar una función con useCallback solo es beneficioso en unos pocos casos:

  • Al enviarla como prop al componente envuelto en memo. Querrás omitir el renderizado subsecuente si el valor no ha cambiado. La memoización permite que tu componente se renderice nuevamente solo cuando las dependencias no sean las mismas.
  • La función que estás enviando se usa más tarde como una dependencia de algún Hook. Por ejemplo, cuando otra función envuelta en useCallback depende de ella, o cuando dependes de dicha función desde useEffect.

No existe ningún beneficio en envolver una función en useCallback en otros casos. Aunque tampoco afecta negativamente hacerlo, por lo que algunos equipos prefieren no enfocarse en los casos de uso individuales y memoizar todo lo posible. La desventaja de este enfoque es que el código se vuelve menos legible. Por otro lado, no toda la memoización es efectiva: un solo valor que «siempre es nuevo» es suficiente para romper la memoización de todo el componente.

Observa que useCallback no evita crear la función. Siempre estás creando una nueva función (¡y eso está bien!), pero React lo ignora y devuelve la función en caché si las dependencias no han cambiado.

En la práctica, puedes hacer que mucha memoización sea innecesaria siguiendo unos pocos principios:

  1. Cuando un componente envuelve visualmente a otros componentes, permite que acepte JSX como hijos. De esta manera, cuando el componente contenedor actualiza su propio estado, React sabe que sus hijos no necesitan volver a renderizarse.
  2. Utiliza el estado local y no eleves el estado más allá de lo necesario. Por ejemplo, no mantengas estados transitorios —como formularios y si a un elemento se le hace hover— en la cima de tu árbol o en una biblioteca de estado global.
  3. Mantén tu lógica de renderizado pura. Si rerenderizar un componente genera un problema o produce algún artefacto visual notable, ¡es un error en tu componente! Arregla el error en lugar de agregar memoización.
  4. Evita Efectos innecesarios que actualizan el estado. La mayor parte de los problemas de rendimiento en aplicaciones de React son causados por cadenas de actualizaciones originadas en Efectos que provocan que tus componentes se rendericen una y otra vez.
  5. Intenta eliminar dependencias innecesarias de tus Efectos. Por ejemplo, en lugar de utilizar la memoización, a menudo es más simple mover algún objeto o función dentro de un Efecto o fuera del componente.

Si una interacción específica aún se siente lenta, utiliza el perfilador de las Herramientas de Desarrollo de React para ver qué componentes se beneficiarían más de la memoización, para agregarla donde sea necesario. Estos principios hacen que tus componentes sean más fáciles de depurar y entender, por lo que es bueno seguirlos en cualquier caso. A largo plazo, estamos investigando el uso de la memoización granular automática para resolver esto de una vez por todas.

La diferencia entre useCallback y declarar la función directamente

Ejemplo 1 de 2:
Omitir rerenderizados con useCallback y memo

En este ejemplo, el componente ShippingForm se ralentiza artificialmente para que puedas ver lo que sucede cuando un componente de React que estás renderizando es realmente lento. Intenta incrementar el contador y cambiar el tema.

Incrementar el contador se siente lento porque obliga al ShippingForm ralentizado a volver a renderizarse. Eso es lo que se espera dado que el contador ha cambiado, y por lo tanto, necesitas reflejar la nueva elección del usuario en la pantalla.

Luego, intenta cambiar el tema. ¡Gracias a useCallback junto con memo, es rápido a pesar del ralentizado artificial! ShippingForm omitió el renderizado subsecuente porque la función handleSubmit no ha cambiado. La función handleSubmit no ha cambiado porque tanto productId como referral (las dependencias de tu useCallback) no han cambiado desde el último renderizado.

import { useCallback } from 'react';
import ShippingForm from './ShippingForm.js';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

function post(url, data) {
  // Imagina que esto envía una petición...
  console.log('POST /' + url);
  console.log(data);
}


Actualizar estado desde un callback en caché

En ocasiones, podrías necesitar actualizar el estado basado en su valor anterior desde un callback en caché.

La función handleAddTodo especifica todos como una dependencia, porque calcula los siguientes todos a partir de ella:

function TodoList() {
const [todos, setTodos] = useState([]);

const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...

Por lo general es mejor que tus funciones almacenadas tengan el menor número de dependencias posibles. Cuando lees un estado solamente para calcular un estado subsecuente, puedes remover esa dependencia al enviar una función de actualización en su lugar:

function TodoList() {
const [todos, setTodos] = useState([]);

const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No se necesita la dependencia `todos`
// ...

Aquí, en lugar de hacer que todos sea una dependencia de tu función y leerla allí, envías a React una instrucción sobre cómo actualizar el estado (todos => [...todos, newTodo]). Lee más sobre las funciones de actualización.


Prevenir que un Efecto se dispare frecuentemente

En ocasiones, es posible que desees llamar a una función desde un Efecto:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}

useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
// ...

Esto genera un problema. Todo valor reactivo debe ser declarado como una dependencia de tu Efecto. Sin embargo, si declaras createOptions como una dependencia, esto provocará que tu Efecto se reconecte constantemente al chat:

useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problema: Esta dependencia cambia en cada renderizado
// ...

Para solventar esto, puedes envolver la función que necesitas llamar desde un Efecto con useCallback:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Solo cambia cuando roomId cambia

useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Solo cambia cuando createOptions cambia
// ...

Esto asegura que la función createOptions sea la misma entre renderizados subsecuentes, siempre que roomId sea el mismo. Sin embargo, es aún mejor eliminar la necesidad de una dependencia de la función. Mueve tu función dentro del Efecto:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
function createOptions() { // ✅ No es necesario usar useCallback ni dependencias de función
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}

const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Solo cambia cuando roomId cambia
// ...

Ahora tu código es mucho más simple y no requiere de useCallback. Aprende más sobre eliminar dependencias de Efectos.


Optimizar un Hook personalizado

Si estás escribiendo un Hook personalizado, es recomendable envolver cualquier función que el Hook retorne con useCallback:

function useRouter() {
const { dispatch } = useContext(RouterStateContext);

const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);

const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);

return {
navigate,
goBack,
};
}

Esto asegura que los consumidores de tu Hook puedan optimizar su propio código cuando sea necesario.


Solución de problemas

Cada ves que mi componente se renderiza, useCallback retorna una función diferente

¡Asegúrate de haber especificado el array de dependencias como un segundo argumento!

Si olvidas el array de dependencias, useCallback retornará una nueva función cada vez:

function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Retorna una función cada vez: no existe un array de dependencias
// ...

Esta es la versión corregida, enviando el array de dependencias como segundo argumento:

function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ No retorna una nueva función innecesariamente
// ...

Si esto no ayuda, entonces el problema es que al menos una de tus dependencias es diferente al renderizado anterior. Puedes depurar este problema manualmente registrando tus dependencias en la consola:

const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);

console.log([productId, referrer]);

Después, puedes hacer click derecho en los arrays de diferentes renderizados en la consola y seleccionar la opción de «Guardar como variable global» para ambos. Suponiendo que el primero se haya guardado con el nombre temp1 y el segundo con el nombre temp2, puedes usar la consola del navegador para verificar si cada dependencia en ambos arrays es la misma:

Object.is(temp1[0], temp2[0]); // ¿Es la primera dependencia la misma entre los arrays?
Object.is(temp1[1], temp2[1]); // ¿Es la segunda dependencia la misma entre los arrays?
Object.is(temp1[2], temp2[2]); // ... y así consecutivamente para cada dependencia ...

Cuando encuentres cuál dependencia está rompiendo la memoización, puedes encontrar una manera de eliminarla o memoízala también.


Necesito llamar useCallback para cada elemento de una lista dentro de un ciclo, pero no es permitido

Suponiendo que el componente Chart está envuelto en memo. Deseas omitir el rerenderizado en cada Chart en la lista cuando el componente ReportList se rerenderiza. Sin embargo, no puedes llamar a useCallback dentro de un ciclo:

function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 No puedes llamar a useCallback dentro de un ciclo así:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);

return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}

En su lugar, extrae un componente para un elemento individual, y coloca useCallback allí:

function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}

function Report({ item }) {
// ✅ Llama a useCallback en el nivel superior:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);

return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}

De forma alternativa, podrías eliminar useCallback en el último fragmento y envolver Report con memo en su lugar. Si la prop item no cambia, Report omitirá el rerenderizado, por lo que Chart también lo hará:

function ReportList({ items }) {
// ...
}

const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}

return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});