useCallback
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]);
Parámetros
-
fn
: La función que deseas guardar en caché. Puede recibir cualquier argumento y devolver cualquier valor. React devolverá (¡no llamará!) tu función durante el renderizado inicial. En los renderizados subsecuentes, React devolverá la misma función nuevamente si lasdependencias
no han cambiado desde el último renderizado. Si no es así, React devolverá 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 devolverá para que puedas decidir si y cuándo llamarla. -
dependencias
: La lista de todos los valores reactivos dentro de la funciónfn
. 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ónObject.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
:
- La definición de la función que quieres almacenar en caché entre rerenderizados.
- 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 devuelta 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
devolverá la misma función que antes. De lo contrario, useCallback
devolverá 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.
Profundizar
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 decomputeRequirements(product)
para que no cambie a menos queproduct
cambie. Esto permite enviar el objetorequirements
sin rerenderizarShippingForm
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 deuseMemo
, no llama a la función recibida. En su lugar, almacena la función que proporcionaste para quehandleSubmit
en sí no cambie a menos queproductId
oreferrer
cambien. Esto permite enviar la funciónhandleSubmit
sin rerenderizarShippingForm
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);
}
Profundizar
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 desdeuseEffect.
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:
- 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.
- 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.
- 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.
- 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.
- 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.
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 devuelva 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
devuelve una función diferente
¡Asegúrate de haber especificado el array de dependencias como un segundo argumento!
Si olvidas el array de dependencias, useCallback
devolverá una nueva función cada vez:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Devuelve 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 devuelve 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>
);
});