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

Detailed shot of a rusty boat hook and rope connection, Bolonia beach.
Foto de Manel Cusido en Pexels

En el día a día de un proyecto React nos topamos con useMemo y useCallback. Ambos prometen evitar renders innecesarios, pero su uso indiscriminado puede acabar peor que no usarlos. En este artículo repasamos la teoría, vemos ejemplos claros y, lo más importante, aprendemos a reconocer los momentos en los que conviene aplicarlos y los que es mejor dejarlos de lado.

Entendiendo la premisa

React vuelve a renderizar un componente cada vez que su state o sus props cambian. Durante ese proceso se vuelven a ejecutar todas las funciones que aparecen dentro del cuerpo del componente, incluidas las que crean valores derivados o funciones de manejo. useMemo guarda el resultado de una función “costosa” y lo devuelve de nuevo mientras sus dependencias no cambien. Por su parte, useCallback guarda la referencia de una función y la mantiene estable entre renders si sus dependencias permanecen iguales.

Cuándo usar useMemo

El gancho aporta beneficio cuando:

  • El cálculo es intensivo (por ejemplo, filtrado o ordenación de grandes arrays).
  • El resultado se pasa como prop a componentes hijos que hacen una comparación === para decidir si deben renderizarse.

Ejemplo típico:

import React, { useMemo, useState } from 'react';

function Lista({ items }) {
  const filtrado = useMemo(() => {
    console.log('Filtrando…');
    return items.filter(i => i.activo);
  }, [items]);

  return (
    <ul>
      {filtrado.map(i => <li key={i.id}>{i.nombre}</li>)}
    </ul>
  );
}

Si items no cambia, la función de filtrado no se vuelve a ejecutar, evitando cálculos innecesarios y manteniendo la referencia del array filtrado estable.

Cuándo usar useCallback

Resulta útil cuando pasas funciones a componentes hijos que dependen de la igualdad de referencia, como React.memo o shouldComponentUpdate. También ayuda a evitar que efectos secundarios (por ejemplo, useEffect) se disparen por una nueva función cada render.

Ejemplo práctico:

import React, { useCallback, useState } from 'react';

function Boton({ onClick }) {
  console.log('Render Boton');
  return <button onClick={onClick}>Pulse</button>;
}

const BotonMemo = React.memo(Boton);

function Padre() {
  const [cont, setCont] = useState(0);

  const incrementar = useCallback(() => setCont(c => c + 1), []);

  return (
    <div>
      <p>Contador: {cont}</p>
      <BotonMemo onClick={incrementar} />
    </div>
  );
}

Sin useCallback, cada render crearía una nueva función incrementar, provocando que BotonMemo se renderizara de nuevo aunque su UI no haya cambiado.

Pitfalls y cuándo evitar

Aunque parezca que useMemo y useCallback son siempre positivos, tienen costes ocultos:

  • Crear la memoización implica almacenar valores en memoria; si el cálculo es barato, el ahorro es insignificante.
  • Las dependencias deben ser precisas; olvidar una variable puede generar bugs difíciles de detectar.
  • El código se vuelve más verboso y menos legible, lo que afecta al mantenimiento.

En la práctica, la regla de oro es medir. Usa la herramienta React DevTools Profiler para identificar renders costosos antes de introducir memoización. Si el componente ya es rápido, es mejor mantenerlo simple.

Conclusión

useMemo y useCallback son armas poderosas en el arsenal de un desarrollador React, pero como cualquier herramienta, su efectividad depende del contexto. Aplica useMemo cuando el cálculo sea costoso y la referencia del resultado sea importante para hijos memoizados. Usa useCallback cuando pases funciones a componentes memoizados o a efectos que dependen de la igualdad de referencia. Y, sobre todo, no los añadas por moda: mide, comprende y decide.