← React & TypeScript

Hooks

¿Para qué sirven useMemo y useCallback? ¿Cuándo los usarías y cuándo serían una optimización prematura?

Ver respuesta — intenta responderla en voz alta primero

useMemo memoriza el RESULTADO de un cálculo y useCallback memoriza una FUNCIÓN, y ambos conservan la misma referencia entre renders mientras sus dependencias no cambien. Se usan cuando un cálculo es caro o cuando la igualdad referencial importa (por ejemplo, con React.memo o dependencias de un effect); en el resto de casos suelen ser ruido.

En cada render, el componente se ejecuta de nuevo: todos los objetos, arreglos y funciones que declaras dentro se crean OTRA VEZ, con referencias nuevas. Para JavaScript, () => {} !== () => {}.

Los puntos clave:

  • useMemo(fn, deps): ejecuta fn y guarda su resultado. En renders siguientes, si las dependencias no cambiaron, devuelve el valor guardado sin recalcular.
  • useCallback(fn, deps): guarda la función misma. useCallback(fn, deps) equivale a useMemo(() => fn, deps).
  • Igualdad referencial: React.memo, los arreglos de dependencias y comparaciones internas usan Object.is. Un objeto o función "nuevos" en cada render rompen esas comparaciones aunque su contenido sea idéntico.
  • Cuándo SÍ: cálculos realmente costosos (filtrar/ordenar listas grandes), o valores/funciones que se pasan a un hijo envuelto en React.memo o que son dependencia de un useEffect.
  • Cuándo NO: memorizar sumas triviales, strings o handlers que van a un <button> normal. Cada memo tiene costo (memoria y comparación de deps) y ensucia el código. Primero mide, luego optimiza.
  • Relación con React.memo: memorizar el callback solo sirve si el hijo que lo recibe está envuelto en React.memo; si no, el hijo se re-renderiza de todas formas.
import { memo, useCallback, useMemo, useState } from "react";

type Producto = { id: number; nombre: string; precio: number };

type FilaProps = { producto: Producto; onSeleccionar: (id: number) => void };

// React.memo: solo se re-renderiza si sus props cambian de referencia/valor
const Fila = memo(function Fila({ producto, onSeleccionar }: FilaProps) {
  return (
    <li onClick={() => onSeleccionar(producto.id)}>
      {producto.nombre} — ${producto.precio}
    </li>
  );
});

function Catalogo({ productos }: { productos: Producto[] }) {
  const [filtro, setFiltro] = useState("");
  const [seleccionado, setSeleccionado] = useState<number | null>(null);

  // useMemo: solo re-filtra cuando cambian productos o filtro,
  // no cuando cambia "seleccionado"
  const visibles = useMemo(
    () => productos.filter(p => p.nombre.toLowerCase().includes(filtro.toLowerCase())),
    [productos, filtro]
  );

  // useCallback: misma referencia entre renders -> React.memo funciona
  const seleccionar = useCallback((id: number) => {
    setSeleccionado(id);
  }, []);

  return (
    <div>
      <input value={filtro} onChange={e => setFiltro(e.target.value)} />
      <p>Seleccionado: {seleccionado ?? "ninguno"}</p>
      <ul>
        {visibles.map(p => (
          <Fila key={p.id} producto={p} onSeleccionar={seleccionar} />
        ))}
      </ul>
      {/* Comportamiento esperado: al seleccionar, las filas NO se re-renderizan */}
    </div>
  );
}

Envolver el hijo en React.memo pero pasarle una función nueva en cada render, anulando la optimización:

// MAL: la arrow function se recrea en cada render del padre,
// así que React.memo en Fila nunca evita nada
<Fila producto={p} onSeleccionar={(id) => setSeleccionado(id)} />

// BIEN: referencia estable con useCallback
const seleccionar = useCallback((id: number) => setSeleccionado(id), []);
<Fila producto={p} onSeleccionar={seleccionar} />

El error opuesto también es común: llenar el componente de useMemo/useCallback "por si acaso". Si nadie compara la referencia y el cálculo es barato, solo agregaste complejidad.

Los dos memorizan cosas entre renders: useMemo guarda el resultado de un cálculo y useCallback guarda la función misma, y ambos mantienen la misma referencia mientras las dependencias no cambien. Importan porque en cada render todo se recrea con referencias nuevas, y eso rompe optimizaciones basadas en igualdad referencial, como React.memo o los arreglos de dependencias. Los uso en dos casos: cálculos costosos, como filtrar listas grandes, y valores o funciones que se pasan a hijos memorizados o a un useEffect. Fuera de eso los considero optimización prematura: tienen su propio costo y prefiero medir antes de memorizar.

Reto rápido

Tienes const numeros: number[] con miles de elementos y un estado tema: "claro" | "oscuro" que cambia seguido. Calcula la suma de los números sin que se recalcule cuando cambia el tema.

Ver solución
import { useMemo, useState } from "react";

function Resumen({ numeros }: { numeros: number[] }) {
  const [tema, setTema] = useState<"claro" | "oscuro">("claro");

  // Solo se recalcula si cambia la referencia de "numeros";
  // cambiar el tema reutiliza el valor memorizado
  const suma = useMemo(() => numeros.reduce((acc, n) => acc + n, 0), [numeros]);

  return (
    <div className={tema}>
      <p>Suma: {suma}</p>
      <button onClick={() => setTema(t => (t === "claro" ? "oscuro" : "claro"))}>Cambiar tema</button>
    </div>
  );
}