useMemo guarda en caché el resultado de un cálculo entre renderizados. React ejecuta tu función en el primer renderizado, conserva el resultado y devuelve ese mismo resultado en los renderizados siguientes hasta que cambia uno de los valores del array de dependencias.
Abre la consola y cambia el tema varias veces: no se registra nada, y el cambio se siente instantáneo. Cambia de pestaña y aparece filtering for active, con una pequeña pausa, porque tab es una dependencia. Ahora reemplaza la línea de useMemo por const visible = filterTodos(todos, tab); y vuelve a cambiar el tema: cada clic registra y se detiene 100 ms.
La sintaxis
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValuees una función sin argumentos que devuelve el valor que quieres guardar en caché. React la llama durante el renderizado.dependencieses la lista de cada valor del componente que lee la función: props, estado y variables declaradas en el cuerpo del componente.- En cada renderizado React compara cada dependencia con su valor del renderizado anterior usando
Object.is. Si todas coinciden, devuelve el valor en caché sin llamar a tu función. Si alguna es distinta, llama a la función y guarda el nuevo resultado.
useMemo es un hook, así que sigue las reglas de los hooks: llámalo en el nivel superior del componente, no dentro de un bucle ni de una condición.
Las dependencias deben estar completas
Deja un valor fuera del array y la caché sigue sirviendo un resultado calculado con su valor viejo. Aquí el total lee qty pero solo incluye price:
Haz clic en Quantity: sube a 2, pero el total se queda en 10 y no se registra nada. Haz clic en Price y el total salta a 22, porque solo entonces se vuelve a ejecutar el cálculo, que ahora lee el qty actual. Agrega qty al array y ambos botones actualizan el total. La regla de lint react-hooks/exhaustive-deps detecta este error en un proyecto real.
El error contrario es una dependencia que es nueva en cada renderizado, como un objeto creado en el cuerpo del componente. Nunca coincide con su valor anterior, así que la caché nunca se usa:
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
Mantener estable un objeto para un hijo con memo
La segunda razón para usar useMemo no tiene nada que ver con cálculos lentos. Un hijo envuelto en memo se salta su renderizado solo cuando cada prop es la misma que la última vez. Un literal de objeto escrito en el cuerpo del componente es un objeto nuevo en cada renderizado, así que anula memo aunque su contenido no haya cambiado. useMemo conserva el mismo objeto hasta que cambian sus entradas.
Escribe en el campo de la nota y Chart se queda en silencio, mientras que Switch unit registra un renderizado. Reemplaza la llamada a useMemo por un simple const options = { unit, max: unit === 'km' ? 100 : 62 }; y cada pulsación en la nota registra render Chart.
Lo mismo se aplica a un efecto que depende de un objeto. Si un efecto incluye options en sus dependencias y options se reconstruye en cada renderizado, el efecto se ejecuta después de cada renderizado. useMemo lo arregla, pero la solución más simple suele ser crear el objeto dentro del efecto y depender de los valores primitivos:
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
Cuándo no usar useMemo
La mayoría de los cálculos de un componente son baratos: sumar números, unir dos cadenas, filtrar unas pocas docenas de elementos. Guardarlos en caché no ahorra casi nada: React igual guarda el valor y compara las dependencias en cada renderizado, y el código se vuelve más difícil de leer.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
Para saber si algo es lento, mide su tiempo. Envuelve el cálculo en console.time('filter') y console.timeEnd('filter'), ejecuta la interacción y mira el número. Como guía aproximada, el trabajo que tarda un milisegundo o más en tu máquina es un candidato, teniendo en cuenta que los dispositivos de tus usuarios pueden ser más lentos. Antes de recurrir a useMemo, comprueba también si el trabajo debe estar en el renderizado: los datos que nunca cambian se pueden calcular una vez fuera del componente, y el estado que usa solo una parte pequeña de la página se puede mover a un componente más pequeño (consulta renderizado).
useMemo vs useEffect para valores derivados
Un patrón habitual es calcular un valor en un efecto y guardarlo en el estado:
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
La versión con efecto renderiza una vez con el visible viejo, luego ejecuta el efecto, cambia el estado y vuelve a renderizar. Es un renderizado extra, y en el primero la lista no coincide con la pestaña. Calcular durante el renderizado con useMemo (o sin ningún hook, si el trabajo es barato) da el valor correcto en el mismo renderizado. Los efectos sirven para sincronizar con cosas fuera de React, no para derivar una pieza de estado a partir de otra.
Una indicación de rendimiento, no una garantía
useMemo le dice a React que puede reutilizar un resultado. No promete que lo hará. React conserva el valor en caché en el funcionamiento normal, pero puede descartarlo, por ejemplo cuando editas el archivo durante el desarrollo, o cuando un componente se suspende durante su primer montaje. Escribe el código para que siga siendo correcto si el cálculo se vuelve a ejecutar: la función debe ser pura, sin efectos secundarios como peticiones, suscripciones o cambios en variables fuera de ella. Los efectos secundarios van en un manejador de eventos o en un efecto.
En desarrollo, con StrictMode, React llama a tu función de cálculo dos veces para ayudarte a detectar código impuro. La vista previa de aquí funciona como un build de producción, así que verás cada registro filtering for una sola vez.
useMemo y el React Compiler
El React Compiler 1.0 es una herramienta de build que analiza tus componentes y agrega memoización automáticamente, guardando en caché cálculos y JSX en los lugares donde ayudarían useMemo, useCallback y memo. En un proyecto que lo usa, escribirás menos de estos a mano. useMemo manual sigue funcionando junto al compilador, sigue siendo la herramienta cuando el compilador no está configurado, y lo encontrarás en la mayor parte del código de React existente.
Preguntas frecuentes
¿Qué hace useMemo en React?
Llama a tu función de cálculo en el primer renderizado, guarda el resultado y, en los renderizados siguientes, devuelve el resultado guardado mientras cada valor del array de dependencias sea el mismo que la última vez (comparado con Object.is).
¿Cuándo debo usar useMemo?
Cuando un cálculo es notablemente lento y sus entradas cambian poco, o cuando necesitas que un objeto o array conserve la misma referencia entre renderizados porque depende de él un hijo con memo o un efecto. Para trabajo barato, sáltatelo.
¿Cuál es la diferencia entre useMemo y useCallback?
useMemo guarda en caché el valor que devuelve tu función. useCallback guarda en caché la función en sí. useCallback(fn, deps) es lo mismo que useMemo(() => fn, deps).
¿useMemo garantiza que el valor nunca se vuelva a calcular?
No. Es una indicación de rendimiento. React puede descartar el valor en caché en algunos casos, por ejemplo en desarrollo cuando editas el archivo, así que tu código debe seguir funcionando bien si el cálculo se vuelve a ejecutar.
¿Todavía necesito useMemo con el React Compiler?
El React Compiler puede agregar memoización automáticamente cuando tu build lo usa. Sin el compilador, o para los casos que omite, useMemo manual sigue funcionando igual y sigue siendo habitual en el código existente.