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

Laptop displaying code outdoors, ideal for remote work or freelance programming.
Foto de Meet Patel en Pexels

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, useMemo evitará que se repita en cada render.
  • Componentes hijos que dependen de referencias: los componentes React.memo o PureComponent hacen 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. useCallback garantiza que la referencia sea estable.
  • Listas de dependencias largas: al crear callbacks que usan varios valores del estado, envolverlos en useCallback evita 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/useCallback si realmente necesitas estabilizar una referencia.
  • Si el componente no está envuelto en React.memo o 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 usa useMemo para 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.