useMemo y useCallback: cuándo optimizan y cuándo estorban

En el día a día de un proyecto React nos encontramos con los hooks useMemo y useCallback. Ambos prometen reducir renders innecesarios, pero su uso indiscriminado puede acabar empeorando el rendimiento o, peor aún, dificultando el mantenimiento.
¿Qué hacen useMemo y useCallback?
useMemo memoriza el resultado de una función "costosa" y lo vuelve a calcular sólo cuando cambian sus dependencias. useCallback es una variante especial: memoriza la propia función, devolviendo la misma referencia entre renders siempre que sus dependencias no cambien.
Cuándo realmente aportan rendimiento
- Cálculos intensivos: si dentro del render ejecutas una función que recorre miles de elementos o hace operaciones matemáticas pesadas,
useMemoevitará que se repita en cada render. - Componentes hijos que dependen de referencias: los componentes
React.memooPureComponenthacen una comparación superficial de props. Si les pasas una función anónima o un objeto nuevo en cada render, siempre se volverán a renderizar.useCallbackgarantiza que la referencia sea estable. - Listas de dependencias largas: al crear callbacks que usan varios valores del estado, envolverlos en
useCallbackevita que la lista cambie innecesariamente.
Cuándo pueden ser una carga
- Overhead de memoría: React guarda el valor o la función entre renders. Si lo usas para valores pequeños o triviales, el coste de crear y almacenar la memoización supera el beneficio.
- Complejidad mental: cada hook introduce una capa extra de abstracción. Cuando el código se vuelve difícil de seguir, el mantenimiento sufre.
- Dependencias incorrectas: olvidar una dependencia o incluir una innecesaria provoca bugs sutiles (cálculos desactualizados o renders infinitos).
Ejemplo práctico
import React, { useState, useMemo, useCallback } from 'react';
function ListaFiltrada({ items }) {
const [query, setQuery] = useState('');
// Sin useMemo: se filtra en cada render
const filtered = items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
// Con useMemo: solo recalcula cuando items o query cambian
const filteredMemo = useMemo(() => {
console.log('Recalculando filtro');
return items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [items, query]);
// Callback para manejar el input, memoizado para evitar re‑creación
const handleChange = useCallback(e => {
setQuery(e.target.value);
}, []);
return (
{filteredMemo.map(item => (
- {item}
))}
);
}
En este caso, useMemo evita ejecutar filter en cada render, algo que se vuelve notorio cuando items contiene cientos o miles de entradas. El useCallback garantiza que handleChange mantenga la misma referencia, lo que es útil si ListaFiltrada se envuelve en React.memo.
Reglas rápidas para decidir
- Si el código dentro del hook es ligero (menos de ~10ms), sólo usa
useMemo/useCallbacksi realmente necesitas estabilizar una referencia. - Si el componente no está envuelto en
React.memoo no pasa callbacks a hijos, el beneficio suele ser nulo. - Siempre revisa la profiling con la DevTools de React: la pestaña "Profiler" te muestra cuánto tiempo se dedica a renders y cuánto a efectos de memoización.
- Evita dependencias "inestables" como objetos creados inline (
{'{'} foo: bar {'}'}) o funciones anónimas; extrae esos valores fuera o usauseMemopara ellos.
Conclusión
useMemo y useCallback son herramientas poderosas, pero como cualquier herramienta de optimización, deben usarse donde hay una necesidad clara. Mide, profila y pregúntate: "¿Este cálculo o función realmente afecta al rendimiento perceptible?" Si la respuesta es no, mejor mantener el código sencillo. Cuando la respuesta es sí, una memoización bien pensada puede reducir renders innecesarios y mejorar la fluidez de tu aplicación.