La lección anterior terminó con un memo sobre TarjetaBicicleta que no ahorra un solo render, porque ListaBicicletas crea funciones flecha nuevas en cada ejecución y la comparación por identidad falla siempre. Ese es el problema que estos dos hooks resuelven, y no es el único: el módulo 7 dejó abierta la misma deuda en tres sitios más —el value de los proveedores de contexto (07-02), las instancias de selectores de Redux (07-05) y las dependencias de los efectos (05-02)—, y todos son la misma cuestión: cómo conseguir que un valor construido dentro de un componente siga siendo el mismo objeto entre renders. A eso se suma un problema distinto que también resuelve useMemo: evitar que un cálculo realmente caro —ordenar y filtrar 2.000 bicicletas— se repita cuando sus entradas no han cambiado. Distinguir esos dos usos es lo más importante de esta lección, porque casi todo el mal uso de la memorización viene de confundirlos. Al final verás además los hooks de concurrencia de React 18/19, useTransition y useDeferredValue, que atacan el mismo síntoma desde un ángulo completamente distinto: en lugar de hacer menos trabajo, reorganizar cuándo se hace.

Contenido

  1. useMemo: firma y qué guarda exactamente
  2. useCallback: el caso particular de useMemo
  3. Los dos hooks, comparados
  4. Uso legítimo 1: evitar un cálculo realmente costoso
  5. Cómo comprobar de verdad si un cálculo es caro
  6. Uso legítimo 2: mantener la identidad de un valor
  7. Cerrando 08-02: ListaBicicletas definitiva
  8. Cerrando 07-02: el value de un proveedor de contexto
  9. Dependencias de useEffect y selectores de Redux
  10. Cómo elegir las dependencias y por qué el linter tiene razón
  11. Cuando una dependencia cambia demasiado
  12. Cuándo NO memorizar
  13. Alternativas mejores que memorizar
  14. Hooks de concurrencia: useTransition y useDeferredValue
  15. El React Compiler aquí

  1. useMemo: firma y qué guarda exactamente

const valorMemorizado = useMemo(() => calcularAlgo(a, b), [a, b]);

Tres partes:

  • La fábrica: una función sin argumentos que devuelve el valor. React la llama; tú no.
  • El array de dependencias: los valores de los que depende el resultado.
  • El valor devuelto: lo que produjo la fábrica.

Y el comportamiento, con precisión:

  1. En el primer render, React ejecuta la fábrica y guarda el resultado junto con las dependencias.
  2. En los siguientes, compara cada dependencia con la guardada usando Object.is.
  3. Si todas coinciden, devuelve el valor guardado sin ejecutar la fábrica.
  4. Si alguna difiere, ejecuta la fábrica otra vez, guarda el nuevo resultado y las nuevas dependencias.

Dos advertencias que cambian cómo se escribe el código:

La fábrica se ejecuta durante el render, así que debe ser pura: nada de peticiones, temporizadores, escrituras en el DOM ni setEstado. Para eso están los efectos (05-02).

useMemo es una optimización, no una garantía. La documentación de React lo dice explícitamente: React puede descartar valores memorizados cuando lo necesite (por ejemplo, para liberar memoria de componentes fuera de pantalla). Nunca escribas código cuya corrección dependa de que la fábrica no se vuelva a ejecutar. Si necesitas que algo ocurra una sola vez de verdad, eso es un useRef o un efecto, no un useMemo.

  1. useCallback: el caso particular de useMemo

const manejarReserva = useCallback((id) => { /* … */ }, [usuario]);

useCallback guarda la función misma, no el resultado de llamarla. Y esta equivalencia lo explica todo:

// Estas dos líneas hacen exactamente lo mismo
const f = useCallback((id) => reservar(id), [reservar]);
const f = useMemo(() => (id) => reservar(id), [reservar]);

useCallback(fn, deps) es useMemo(() => fn, deps). Existe como hook propio solo porque memorizar funciones es tan frecuente que la doble flecha resultaba incómoda y propensa a errores.

El error de principiante que hay que evitar desde el primer día:

// ❌ Llama a la función y memoriza su RESULTADO
const total = useCallback(calcularTotal(horas), [horas]);

// ✅ Memoriza la FUNCIÓN
const calcular = useCallback(() => calcularTotal(horas), [horas]);

// ✅ Memoriza el RESULTADO — que es lo que probablemente querías
const total = useMemo(() => calcularTotal(horas), [horas]);

  1. Los dos hooks, comparados

useMemo useCallback
Qué guarda El valor devuelto por la fábrica La función que le pasas
Firma useMemo(() => valor, deps) useCallback(fn, deps)
Cuándo recalcula Cuando cambia alguna dependencia Cuando cambia alguna dependencia
Equivalencia useMemo(() => fn, deps)
Motivo 1: coste ✅ Su razón principal ❌ Crear una función es baratísimo
Motivo 2: identidad ✅ Objetos, arrays, instancias ✅ Su razón única
Uso típico Filtrar/ordenar listas, value de contexto, objetos de opciones Manejadores pasados a componentes memorizados o a dependencias de efectos
La fábrica se ejecuta Durante el render Nunca la ejecuta React: solo la guarda

Un matiz que ordena la tabla: useCallback nunca es una optimización de coste. Crear una función en JavaScript cuesta prácticamente nada. useCallback existe solo para preservar la identidad. Si nadie compara esa función —no va a un componente memorizado, no es dependencia de un efecto ni de otro hook—, el useCallback es coste puro.

  1. Uso legítimo 1: evitar un cálculo realmente costoso

Este es el catálogo de CicloUrbano con las 2.000 bicicletas, filtrando y ordenando en el render:

// src/paginas/PaginaCatalogo.jsx (sin memorizar)
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router';
import { useBicicletas, useEstaciones } from '../consultas/consultasBicicletas.js';

function PaginaCatalogo() {
  const { data: bicicletas = [] } = useBicicletas();
  const { data: estaciones = [] } = useEstaciones();
  const termino = useSelector((estado) => estado.catalogo.termino);
  const orden = useSelector((estado) => estado.catalogo.orden);
  const [parametros] = useSearchParams();
  const tipo = parametros.get('tipo') ?? 'todos';

  // Este bloque se ejecuta en CADA render, cambie lo que cambie
  const visibles = bicicletas
    .filter((b) => tipo === 'todos' || b.tipo === tipo)
    .filter((b) => b.modelo.toLowerCase().includes(termino.toLowerCase()))
    .map((b) => ({
      ...b,
      nombreEstacion: estaciones.find((e) => e.id === b.estacionId)?.nombre ?? '—'
    }))
    .sort((a, b) =>
      orden === 'precio' ? a.precioHora - b.precioHora : a.modelo.localeCompare(b.modelo)
    );

  return (
    <>
      <BuscadorBicicletas />
      <SelectorTipo />
      <ListaBicicletas bicicletas={visibles} />
    </>
  );
}

Dos problemas independientes, y conviene no mezclarlos:

  • Coste: con 2.000 bicicletas hay dos filtrados, un map que hace un find sobre las estaciones por cada elemento —eso es O(n × m)— y una ordenación con localeCompare, que es de las comparaciones más caras de JavaScript. Entre 10 y 40 ms por render en un equipo de desarrollo, y tres o cuatro veces más en un móvil.
  • Identidad: visibles es un array nuevo en cada render, así que cualquier memo sobre ListaBicicletas estaría condenado a fallar.

useMemo resuelve los dos a la vez:

const visibles = useMemo(() => {
  // 1) Índice de estaciones: convierte el find O(m) en un acceso O(1)
  const nombrePorEstacion = new Map(estaciones.map((e) => [e.id, e.nombre]));
  // 2) Colador reutilizable: crear un Intl.Collator por comparación es carísimo
  const colador = new Intl.Collator('es');
  const terminoNormalizado = termino.trim().toLowerCase();

  return bicicletas
    .filter((b) => tipo === 'todos' || b.tipo === tipo)
    .filter((b) => b.modelo.toLowerCase().includes(terminoNormalizado))
    .map((b) => ({ ...b, nombreEstacion: nombrePorEstacion.get(b.estacionId) ?? '—' }))
    .sort((a, b) =>
      orden === 'precio' ? a.precioHora - b.precioHora : colador.compare(a.modelo, b.modelo)
    );
}, [bicicletas, estaciones, termino, tipo, orden]);

Presta atención a lo que ha pasado aquí, porque es la lección de fondo: antes de memorizar, se ha reducido el trabajo. El Map de estaciones convierte una búsqueda lineal por elemento en un acceso directo, y el Intl.Collator reutilizado evita construir un comparador en cada llamada de sort. Esas dos líneas bajan el coste más que el useMemo, y siguen sirviendo aunque las dependencias cambien en cada render. La memorización llega después, no en lugar de.

Con el useMemo puesto, esta es la tabla de comportamiento:

Qué cambia ¿Recalcula? ¿Correcto?
El usuario escribe en el buscador Sí (termino) Sí: el resultado depende de ello
Cambia ?tipo= en la URL Sí (tipo)
Cambia el orden Sí (orden)
Query revalida y devuelve los mismos datos Sí (bicicletas es un array nuevo) Inevitable: Query sustituye la referencia
Se abre el Modal (estado local de la página) No Esa es la ganancia
Cambia el tema No Ídem
Llega un aviso nuevo No Ídem

  1. Cómo comprobar de verdad si un cálculo es caro

«Caro» no es una opinión. Se mide, y hay dos formas rápidas antes de abrir el Profiler.

Con console.time, envolviendo el cálculo sin memorizar:

const visibles = useMemo(() => {
  console.time('filtrar y ordenar catálogo');
  const resultado = /* … el cálculo … */;
  console.timeEnd('filtrar y ordenar catálogo');
  return resultado;
}, [bicicletas, estaciones, termino, tipo, orden]);

Cómo interpretar el número que salga por consola:

Duración medida Veredicto
< 1 ms No memorices. El useMemo cuesta más que el cálculo
1 – 5 ms Zona gris: depende de con qué frecuencia ocurra y de si hay más trabajo en el mismo render
5 – 16 ms Memorizar probablemente compensa: te acercas al presupuesto de un fotograma
> 16 ms Memoriza, y además revisa el algoritmo: probablemente hay un find dentro de un map

La referencia de los 16 ms viene de que a 60 fotogramas por segundo el navegador dispone de 16,7 ms por fotograma. Todo lo que se pase de ahí produce un salto visible.

Con la ralentización de CPU de las herramientas del navegador. En la pestaña Rendimiento (Performance) hay un selector de CPU throttling: ponlo en o . Es lo más parecido a un móvil de gama media que tienes sin comprar uno. Un cálculo de 4 ms en tu portátil pasa a 16–24 ms ahí, y lo que era irrelevante deja de serlo.

Y el paso que casi nadie da: quita el useMemo y mide otra vez. Si el número no cambia de forma perceptible, quítalo definitivamente. Es el paso «volver a medir» del flujo de trabajo de 08-01, y es lo que distingue optimizar de decorar.

  1. Uso legítimo 2: mantener la identidad de un valor

El segundo uso no tiene nada que ver con el coste del cálculo. Aquí la fábrica puede ser trivial —() => ({ usuario, esOperario })— y aun así el useMemo es imprescindible, porque lo que importa no es lo que cuesta construir el valor, sino que alguien más abajo lo va a comparar por identidad.

¿Quién compara? Exactamente estos cuatro:

Quién compara Qué pasa si la identidad cambia
memo en un componente hijo La comparación falla y el componente se reejecuta siempre (08-02)
useEffect con esa dependencia El efecto se vuelve a ejecutar: petición repetida, temporizador reiniciado, suscripción rehecha
Un proveedor de contexto (value) Todos los consumidores se reejecutan, usen o no la parte que cambió (07-02)
useSelector de Redux o un selector memorizado Repintado en cada acción despachada, o memorización que nunca acierta (07-05)

Y hay un quinto, silencioso: otro useMemo o useCallback que tenga ese valor entre sus dependencias. Una identidad inestable se propaga en cascada e invalida toda la memorización que hay por debajo. Por eso los problemas de identidad se arreglan desde arriba: estabiliza primero el valor de más arriba y muchos de los de abajo se arreglan solos.

  1. Cerrando 08-02: ListaBicicletas definitiva

Este es el código que 08-02 dejó pendiente.

// src/componentes/ListaBicicletas.jsx (versión definitiva)
import { useCallback } from 'react';
import { useNavigate } from 'react-router';
import TarjetaBicicleta from './TarjetaBicicleta.jsx';
import estilos from './ListaBicicletas.module.css';

function ListaBicicletas({ bicicletas }) {
  const navegar = useNavigate();

  // navegar es estable: React Router garantiza su identidad entre renders.
  // Con [navegar] como dependencia, estas funciones se crean UNA vez.
  const manejarSeleccion = useCallback(
    (id) => navegar(`/bicicletas/${id}`),
    [navegar]
  );

  const manejarReserva = useCallback(
    (id) => navegar(`/reservas/nueva?bicicleta=${id}`),
    [navegar]
  );

  return (
    <ul className={estilos.rejilla}>
      {bicicletas.map((bicicleta) => (
        <li key={bicicleta.id}>
          <TarjetaBicicleta
            bicicleta={bicicleta}
            nombreEstacion={bicicleta.nombreEstacion}
            alSeleccionar={manejarSeleccion}
            alReservar={manejarReserva}
          />
        </li>
      ))}
    </ul>
  );
}

export default ListaBicicletas;

Tres decisiones y sus motivos:

  1. useCallback con [navegar]. La función navegar de React Router es estable por diseño, así que ambas funciones se crean una sola vez en toda la vida del componente. Ahora Object.is(propsPrevias.alSeleccionar, propsNuevas.alSeleccionar) da true y el memo de 08-02 por fin funciona.
  2. nombreEstacion viene ya calculado desde el useMemo de PaginaCatalogo, en lugar de resolverse aquí con un find por cada tarjeta. Menos trabajo y una prop primitiva más.
  3. Los manejadores reciben el id como argumento, no se crea una función por tarjeta. Si TarjetaBicicleta necesitara () => alReservar(bicicleta.id), esa flecha se crearía dentro de la tarjeta, que es donde debe estar: es una función que la tarjeta pasa a un elemento del DOM, y a un onClick de un <button> le da igual la identidad.

Este es el escenario donde memo y useCallback funcionan: juntos, en una lista larga, con una medición previa que lo justifica. Ninguno de los dos sirve de nada por separado.

  1. Cerrando 07-02: el value de un proveedor de contexto

La deuda que el módulo 7 dejó explícitamente pendiente. El problema, recordado en una línea: el value de un proveedor es un objeto literal creado en cada render, así que cada render del proveedor reejecuta a todos sus consumidores, aunque el contenido sea idéntico.

// ❌ El problema
export function ProveedorTema({ children }) {
  const [tema, setTema] = useAlmacenLocal('tema', 'claro');

  // Objeto NUEVO en cada render → todos los consumidores se reejecutan
  const valor = { tema, alternarTema: () => setTema((t) => (t === 'claro' ? 'oscuro' : 'claro')) };

  return <ContextoTema value={valor}>{children}</ContextoTema>;
}
// ✅ La versión definitiva de src/contextos/ContextoTema.jsx
import { createContext, useContext, useMemo, useCallback, useEffect } from 'react';
import { useAlmacenLocal } from '../hooks/useAlmacenLocal.js';

const ContextoTema = createContext(null);

export function ProveedorTema({ children }) {
  const [tema, setTema] = useAlmacenLocal('tema', 'claro');

  useEffect(() => {
    document.documentElement.dataset.tema = tema;
  }, [tema]);

  // 1) La función, estable: setTema de useState es estable por contrato de React
  const alternarTema = useCallback(() => {
    setTema((actual) => (actual === 'claro' ? 'oscuro' : 'claro'));
  }, [setTema]);

  // 2) El objeto, estable mientras no cambie el tema
  const valor = useMemo(() => ({ tema, alternarTema }), [tema, alternarTema]);

  return <ContextoTema value={valor}>{children}</ContextoTema>;
}

export function useTema() {
  const contexto = useContext(ContextoTema);
  if (contexto === null) {
    throw new Error('useTema debe usarse dentro de <ProveedorTema>');
  }
  return contexto;
}

Qué consigue y qué no, con precisión:

  • Consigue: si ProveedorTema se reejecuta por un motivo ajeno al tema —por ejemplo, porque su padre se repintó—, valor es el mismo objeto, Object.is da true y React no notifica a ningún consumidor.
  • No consigue: cuando el tema cambia, todos los consumidores se reejecutan, incluidos los que solo usan alternarTema. Para eso está la división de contextos en estado y acciones de 07-02, que es una solución estructural y complementaria.

Aplicado a ContextoAvisos, donde la división ya existe, el resultado es el patrón completo:

// src/contextos/ContextoAvisos.jsx (fragmento con la memorización completa)
export function ProveedorAvisos({ children }) {
  const [avisos, setAvisos] = useState([]);

  const descartarAviso = useCallback((id) => {
    setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
  }, []);

  const mostrarAviso = useCallback((tono, texto, duracion = 5000) => {
    const id = `aviso-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previos) => [...previos, { id, tono, texto }]);
    if (duracion > 0) setTimeout(() => descartarAviso(id), duracion);
    return id;
  }, [descartarAviso]);

  // Las ACCIONES no cambian NUNCA: sus consumidores no se reejecutan jamás
  const acciones = useMemo(
    () => ({ mostrarAviso, descartarAviso }),
    [mostrarAviso, descartarAviso]
  );

  return (
    <ContextoAccionesAvisos value={acciones}>
      <ContextoEstadoAvisos value={avisos}>
        {children}
      </ContextoEstadoAvisos>
    </ContextoAccionesAvisos>
  );
}

El resultado combinado es exactamente lo que se buscaba en 07-02: un componente como FormularioReserva, que solo llama a mostrarAviso y nunca lee la lista, no se reejecuta nunca por culpa de los avisos. Antes se reejecutaba con cada aviso que apareciera o desapareciera en cualquier punto de la aplicación.

Regla operativa, ahora justificada: todo proveedor que publique un objeto literal como value debe estabilizarlo con useMemo, y las funciones que contenga con useCallback. Es de los poquísimos sitios donde memorizar es la opción por defecto y no una optimización prematura, porque el coste de no hacerlo se propaga a toda la aplicación.

  1. Dependencias de useEffect y selectores de Redux

En efectos (retomando 05-02): una función o un objeto en el array de dependencias de un useEffect provoca que el efecto se vuelva a ejecutar en cada render.

// ❌ Bucle de peticiones: opciones es un objeto nuevo cada render
function PanelIncidencias({ estacionId }) {
  const opciones = { estacionId, incluirCerradas: false };

  useEffect(() => {
    cargarIncidencias(opciones).then(setIncidencias);
  }, [opciones]);              // cambia SIEMPRE → petición en cada render
}

// ✅ Opción A: estabilizar el objeto
const opciones = useMemo(() => ({ estacionId, incluirCerradas: false }), [estacionId]);

// ✅ Opción B, mejor: no crear el objeto fuera del efecto
useEffect(() => {
  cargarIncidencias({ estacionId, incluirCerradas: false }).then(setIncidencias);
}, [estacionId]);              // solo primitivos

La opción B es casi siempre preferible, y esa es la enseñanza: cuando una dependencia inestable causa problemas en un efecto, la primera pregunta no es «¿cómo la memorizo?» sino «¿por qué está fuera del efecto?».

En selectores de Redux (retomando 07-05): un selector creado en línea que construye un valor devuelve una referencia nueva en cada llamada, y useSelector compara por identidad, así que el componente se repinta con cada acción despachada en la aplicación.

// ❌ Objeto nuevo en cada ejecución del selector
const { activas, total } = useSelector((estado) => ({
  activas: estado.reservas.ids.filter((id) => estado.reservas.entidades[id].estado === 'activa'),
  total: estado.reservas.ids.length
}));

// ✅ Selector memorizado en el slice (createSelector), leído sin construir nada
const resumen = useSelector(seleccionarResumenReservas);

// ✅ Fábrica de selectores con argumento, con su instancia estabilizada
const selectorReservas = useMemo(crearSelectorReservasDeUsuario, []);
const reservas = useSelector((estado) => selectorReservas(estado, usuarioId));

La distinción entre las tres capas de memorización que ahora conviven en CicloUrbano:

Capa Herramienta Ámbito Qué evita
Almacén createSelector Global, compartido por todos los componentes Recalcular derivaciones del estado de Redux
Componente useMemo Una instancia de un componente Recalcular en cada render de ese componente
Servidor staleTime de Query Global, por clave de consulta Volver a pedir datos al servidor

  1. Cómo elegir las dependencias y por qué el linter tiene razón

La regla es exacta y no admite excepciones: el array de dependencias debe contener todos los valores reactivos que la fábrica lee. Valor reactivo es cualquier cosa declarada dentro del componente que pueda cambiar entre renders: props, estado, valores de contexto, y cualquier variable derivada de ellos.

function PanelReserva({ bicicleta, descuento }) {
  const [horas, setHoras] = useState(1);
  const { usuario } = useSesion();

  const importe = useMemo(
    () => bicicleta.precioHora * horas * (1 - descuento),
    [bicicleta.precioHora, horas, descuento]   // los tres valores que lee, ninguno más
  );
  // `usuario` NO es dependencia: la fábrica no lo lee
}

Qué pasa si te equivocas, en cada dirección:

Error Consecuencia Gravedad
Falta una dependencia El valor se queda congelado con datos viejos. La pantalla miente Grave: es un fallo de corrección
Sobra una dependencia Recalcula de más. Solo se pierde la optimización Leve
Array vacío [] en un valor que depende de props Congelado para siempre Muy grave
Sin array (omitirlo) Recalcula siempre: el useMemo no hace nada Leve, pero es código muerto

Por eso eslint-plugin-react-hooks con la regla exhaustive-deps no es opcional en un proyecto serio, y por eso silenciarla con un comentario casi siempre esconde un problema de diseño:

// ❌ La señal de alarma más fiable del ecosistema React
// eslint-disable-next-line react-hooks/exhaustive-deps

Cuando sientas la tentación de escribir esa línea, el problema real suele ser uno de estos tres: la fábrica hace algo que debería estar en un efecto, la dependencia debería estabilizarse en el componente padre, o el valor no debería estar en el estado.

  1. Cuando una dependencia cambia demasiado

Este es el caso difícil: has puesto las dependencias correctas y una de ellas cambia en cada render, con lo que la memorización nunca acierta. Cuatro salidas, ordenadas de mejor a peor:

1. Sacar lo que no depende de nada fuera del componente.

// ✅ Constantes y funciones puras: al ámbito del módulo
const TIPOS_PERMITIDOS = ['urbana', 'electrica', 'carga'];
const FORMATO_EURO = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' });

function calcularImporte(precioHora, horas) {
  return precioHora * horas;
}

function PanelReserva({ bicicleta }) {
  // Ni useMemo ni useCallback: estos valores no se crean nunca más de una vez
}

Es la mejor solución con diferencia: cero memorización, cero dependencias, cero riesgo. Antes de memorizar cualquier cosa, pregúntate si depende realmente de algo del componente.

2. Mover la función dentro de quien la usa. Si manejarEnvio solo la usa un efecto, defínela dentro del efecto y desaparece del array de dependencias (05-02).

3. Usar useReducer en lugar de varios useState. La función despachar que devuelve useReducer es estable por contrato: React garantiza que no cambia entre renders. Eso permite pasarla directamente a componentes memorizados sin ningún useCallback.

// El estado del formulario de reserva, con dispatch estable
const [borrador, despachar] = useReducer(reductorBorrador, BORRADOR_INICIAL);

// `despachar` es estable: memo funciona sin useCallback
<FormularioReserva borrador={borrador} alDespachar={despachar} />

Lo mismo vale para las funciones setX de useState y para el dispatch de react-redux: todas son estables y no necesitan useCallback nunca.

4. Usar una referencia para valores que no deben provocar recálculo. Es el último recurso, con cuidado, y solo para valores que se leen en manejadores o efectos, nunca durante el render:

const alReservarRef = useRef(alReservar);
useEffect(() => { alReservarRef.current = alReservar; });   // siempre al día

const manejarClic = useCallback((id) => {
  alReservarRef.current(id);      // lee la versión más reciente sin depender de ella
}, []);                           // identidad estable de por vida

Este patrón —el «evento efectivo»— es útil, pero rompe el flujo de datos explícito y complica la lectura. Úsalo cuando las tres opciones anteriores no sirvan, no antes.

  1. Cuándo NO memorizar

No memorices Por qué
Valores primitivos (const total = precio * horas) Multiplicar es más barato que consultar la memorización
Cálculos triviales (array.length, una concatenación, un Boolean()) El hook cuesta más que el cálculo
Filtros sobre arrays cortos (menos de ~100 elementos y sin trabajo por elemento) Recorrer 20 objetos es ruido estadístico
Funciones que solo van a un elemento del DOM (onClick de un <button>) Al DOM le da igual la identidad de la función
Funciones que solo usa el propio componente Nadie las compara
Props de un componente que no está memorizado No hay ninguna comparación que aprovechar
Componentes que se renderizan una vez No hay renders repetidos que ahorrar
«Por si acaso» Es la definición literal de optimización prematura

El caso más frecuente y el más inútil de todos:

// ❌ useCallback sin nadie que compare
function PaginaAcceso() {
  const manejarEnvio = useCallback((evento) => {
    evento.preventDefault();
    iniciarSesion(usuario);
  }, [usuario]);

  return <form onSubmit={manejarEnvio}>…</form>;   // un <form> del DOM: no compara nada
}

Aquí useCallback añade un array de dependencias que mantener, una comparación por render y memoria, a cambio de exactamente nada. La versión correcta es una función normal.

Y la cuenta honesta del coste, que rara vez se hace explícita: cada useMemo o useCallback supone una llamada al hook, un array creado en cada render, una comparación por dependencia y memoria retenida mientras el componente viva. Es poco, pero es distinto de cero, y multiplicado por cientos de usos innecesarios es medible.

  1. Alternativas mejores que memorizar

Antes de escribir un useMemo, recorre esta tabla. Cada fila es una técnica que resuelve el mismo problema sin dependencias que mantener.

Situación En vez de memorizar Por qué es mejor
Un estado hace repintar un subárbol grande Bajar el estado (08-01) Elimina el render en lugar de saltárselo. Menos código
Un contenedor con estado envuelve contenido caro Pasar children (08-01) Aislamiento estructural, sin comparación ni memoria
El valor no depende de nada del componente Sacarlo fuera del componente Se crea una vez en toda la aplicación
Varios useState con manejadores que se pasan hacia abajo useReducer despachar es estable por contrato
Un cálculo caro sobre el estado de Redux createSelector en el slice Memorización compartida por todos los componentes
Un cálculo caro sobre datos del servidor select de useQuery Se calcula al llegar el dato, no en cada render
Una lista larguísima Paginar o virtualizar (08-01) Reduce el trabajo real, no lo cachea
El resultado depende solo del identificador Índice Map construido una vez Cambia la complejidad del algoritmo
Un filtrado que bloquea el tecleo useDeferredValue (apartado 14) Reordena el trabajo en vez de evitarlo

El árbol de decisión completo, que resume los apartados 4 a 13:

flowchart TD
    A["Voy a escribir un useMemo<br/>o un useCallback"] --> B{"Depende de algo<br/>del componente?"}
    B -->|No| C["Sacalo FUERA del componente<br/>constante o funcion pura del modulo"]
    B -->|Si| D{"Por que quiero<br/>memorizar?"}

    D -->|"El calculo es caro"| E{"Lo he medido?<br/>console.time + CPU 4x"}
    E -->|No| F["Mide primero.<br/>Por debajo de 1 ms: no memorices"]
    E -->|"Si, mas de 5 ms"| G{"Puedo reducir el<br/>trabajo en vez de cachearlo?"}
    G -->|Si| H["Indice Map, Collator reutilizado,<br/>paginar, select de useQuery"]
    G -->|No| I["useMemo justificado"]

    D -->|"Alguien compara<br/>la identidad"| J{"Quien compara?"}
    J -->|Nadie| K["No memorices:<br/>coste puro"]
    J -->|"Componente con memo"| L["useCallback / useMemo<br/>y comprueba que memo esta puesto"]
    J -->|"Dependencia de useEffect"| M["Mejor: mueve el valor<br/>DENTRO del efecto"]
    J -->|"value de un contexto"| N["useMemo + useCallback:<br/>aqui es la opcion por defecto"]
    J -->|"Selector de Redux"| O["createSelector en el slice"]

  1. Hooks de concurrencia: useTransition y useDeferredValue

Los dos hooks anteriores intentan hacer menos trabajo. Los de concurrencia hacen algo distinto: dejan que React interrumpa y posponga trabajo no urgente para que la interfaz siga respondiendo. React puede empezar a renderizar una actualización marcada como no urgente, abandonarla a media si llega una entrada del usuario, atender esa entrada y retomar después.

useTransition

const [estaPendiente, iniciarTransicion] = useTransition();

Devuelve un booleano —«hay una transición en curso»— y una función para envolver las actualizaciones no urgentes.

// src/componentes/SelectorTipo.jsx
import { useTransition } from 'react';
import { useSearchParams } from 'react-router';

function SelectorTipo() {
  const [parametros, setParametros] = useSearchParams();
  const [estaPendiente, iniciarTransicion] = useTransition();
  const tipo = parametros.get('tipo') ?? 'todos';

  function manejarCambio(evento) {
    const nuevoTipo = evento.target.value;

    // No urgente: repintar 2.000 tarjetas puede esperar e interrumpirse
    iniciarTransicion(() => {
      setParametros(nuevoTipo === 'todos' ? {} : { tipo: nuevoTipo });
    });
  }

  return (
    <select value={tipo} onChange={manejarCambio} aria-busy={estaPendiente}>
      <option value="todos">Todos los tipos</option>
      <option value="urbana">Urbana</option>
      <option value="electrica">Eléctrica</option>
      <option value="carga">Carga</option>
    </select>
  );
}

Lo que cambia: sin la transición, elegir «Eléctrica» congela el <select> mientras React filtra y pinta el catálogo. Con ella, el desplegable responde al instante, estaPendiente permite atenuar la lista mientras se recalcula, y si el usuario cambia de opción otra vez, React abandona el render a medias y empieza el nuevo.

Requisito importante: la actualización debe ir dentro de iniciarTransicion y ser síncrona. Y no funciona con campos de texto controlados: el valor de un <input> es una actualización urgente por definición y marcarla como transición produce un campo que se siente roto.

useDeferredValue

const valorDiferido = useDeferredValue(valor);

Devuelve una copia del valor que se queda atrás a propósito. React renderiza primero con el valor viejo (rápido, urgente) y después, en segundo plano y de forma interrumpible, con el nuevo.

// src/paginas/PaginaCatalogo.jsx (fragmento)
function PaginaCatalogo() {
  const termino = useSelector((estado) => estado.catalogo.termino);
  const terminoDiferido = useDeferredValue(termino);
  const estaObsoleto = termino !== terminoDiferido;

  const visibles = useMemo(
    () => filtrarYOrdenar(bicicletas, estaciones, terminoDiferido, tipo, orden),
    [bicicletas, estaciones, terminoDiferido, tipo, orden]
  );

  return (
    <>
      <BuscadorBicicletas />
      <div style={{ opacity: estaObsoleto ? 0.6 : 1, transition: 'opacity 150ms' }}>
        <ListaBicicletas bicicletas={visibles} />
      </div>
    </>
  );
}

La combinación de useDeferredValue + useMemo + memo es el patrón completo: el valor diferido reduce cuántas veces se recalcula durante el tecleo rápido, el useMemo evita recalcular cuando nada relevante cambió, y el memo de TarjetaBicicleta evita reejecutar las tarjetas que no se han movido. Ninguno sustituye a los otros.

Los tres, comparados

useDebounce (05-06) useTransition useDeferredValue
Qué problema resuelve Trabajo demasiado frecuente Trabajo urgente mezclado con no urgente Un valor cuyo consumo es caro
Mecanismo Temporizador: espera al silencio Marca la actualización como interrumpible Renderiza en dos pasadas, la segunda diferible
Retraso Fijo, lo eliges tú (300 ms) Ninguno: se adapta a la carga real Ninguno fijo: se adapta
Interrumpible No: cuando se ejecuta, bloquea
Evita peticiones al servidor , es su punto fuerte No No
Indicador de progreso Manual estaPendiente Comparar valor y valor diferido
Dónde se coloca En el productor del valor En quien provoca la actualización En quien consume el valor
En CicloUrbano Buscador → petición a json-server SelectorTipo, cambio de orden Filtrado local del catálogo

La conclusión práctica, que es más útil que la tabla: no son alternativas, son complementarios. En el BuscadorBicicletas definitivo conviven los dos: useDebounce para no lanzar una petición por tecla —eso es tráfico de red y el temporizador es la herramienta correcta— y useDeferredValue sobre el término para que el filtrado local de las 2.000 bicicletas no bloquee el campo entre petición y petición.

  1. El React Compiler aquí

El React Compiler de React 19 es, en esencia, un generador automático de useMemo y useCallback. En un proyecto compilado:

Trabajo ¿Lo hace el compilador?
Estabilizar manejarSeleccion y manejarReserva en ListaBicicletas ✅ Sí, sin useCallback
Estabilizar el objeto valor de ProveedorTema ✅ Sí, si se construye en el propio componente
Evitar repetir el filtrado y la ordenación cuando las entradas no cambian ✅ Sí, memoriza la expresión
Hacer que la primera ordenación de 2.000 bicicletas sea rápida No. El algoritmo sigue siendo tuyo
Sustituir el Map de estaciones por un find en cada elemento No: no reescribe algoritmos
Saber que una función importada de otro módulo es estable No: no razona fuera del componente
Decidir useTransition o useDeferredValue No: son decisiones de experiencia de usuario
Corregir un useEffect que se dispara de más por una dependencia externa No
Optimizar un componente que rompe las reglas de React No: lo omite entero y en silencio

Cómo comprobar qué hizo. El compilador no es una caja negra:

  • React DevTools marca los componentes optimizados con una insignia ✨ Memo ✨ junto a su nombre. Es la comprobación más rápida: si tu componente crítico no la tiene, el compilador lo ha omitido y hay que averiguar por qué.
  • El Profiler (08-05) sigue siendo la prueba definitiva: mide la misma interacción con y sin compilador en un build de producción y compara.
  • El linter (eslint-plugin-react-hooks con las reglas del compilador) te dice de antemano qué componentes se van a omitir, y por qué.

Y la parte que sigue siendo tuya, resumida en una frase: el compilador decide cuándo reutilizar un valor; tú decides qué valor merece la pena calcular, con qué algoritmo, y si ese trabajo debería estar ocurriendo siquiera.

Errores Comunes y Consejos

Confundir los dos usos legítimos. «Memorizo porque es caro» y «memorizo porque alguien compara la identidad» son razones distintas y llevan a decisiones distintas. Un useMemo sobre un cálculo trivial es inútil… salvo que el resultado sea un objeto que va a un componente memorizado, en cuyo caso es imprescindible. Ten siempre clara cuál de las dos razones te aplica.

useCallback sobre funciones que nadie compara. Es el uso mayoritario y el más inútil. Si la función va a un onClick de un <button>, quítalo.

Silenciar exhaustive-deps. Un useMemo con dependencias incompletas produce datos obsoletos en pantalla: un fallo de corrección, no de rendimiento. Si el linter te molesta, el problema está en el diseño.

Memorizar dentro de un .map(). Los hooks no pueden llamarse en bucles (regla de 04-04). Si cada elemento necesita memorización, la memorización va dentro del componente hijo, no en el padre.

Creer que useMemo garantiza la ejecución única. React puede descartar el valor guardado. No pongas ahí nada de lo que dependa la corrección de tu código.

Usar useTransition en un campo de texto controlado. El valor del <input> es urgente por definición; diferirlo produce un campo que se siente estropeado. Lo que se difiere es su consumo, con useDeferredValue.

Consejo: primero el algoritmo, después la memorización. El Map de estaciones y el Intl.Collator reutilizado bajan más el coste que cualquier useMemo, y siguen funcionando cuando las dependencias cambian de verdad.

Consejo: estabiliza desde arriba. Una identidad inestable invalida toda la memorización que hay por debajo. Arregla el value del proveedor y el objeto de opciones del componente raíz antes de tocar las hojas.

Consejo: memoriza el paquete entero, no las piezas. En lugar de tres useMemo para tres campos, uno solo para el objeto que los contiene, si es eso lo que se va a comparar.

Ejercicios

Ejercicio 1. Para cada uso, di si el hook es necesario, sobra o está mal escrito, y corrige los dos últimos casos.

// a)
const total = useMemo(() => horas * bicicleta.precioHora, [horas, bicicleta.precioHora]);

// b)
const bicicletasOrdenadas = useMemo(
  () => [...bicicletas].sort((x, y) => x.precioHora - y.precioHora),
  [bicicletas]
);
// bicicletas tiene 2.000 elementos y se pasa a <ListaBicicletas> (memorizada)

// c)
const manejarCierre = useCallback(() => setModalAbierto(false), []);
// Uso: <Modal alCerrar={manejarCierre} />  ← Modal NO está memorizado

// d)
const filtradas = useCallback(bicicletas.filter((b) => b.estado === 'disponible'), [bicicletas]);

// e)
const valor = useMemo(() => ({ usuario, iniciarSesion, cerrarSesion }), []);
// Uso: <ContextoSesion value={valor}>

Ejercicio 2. PaginaDetalleEstacion vuelve a pedir las incidencias a json-server en cada render, con lo que la pestaña parpadea sin parar. Diagnostica la causa exacta y propón dos soluciones: una con memorización y otra sin ella. Indica cuál elegirías y por qué.

function PaginaDetalleEstacion() {
  const { estacionId } = useParams();
  const [incidencias, setIncidencias] = useState([]);
  const filtros = { estacionId, estado: 'abierta', orden: 'fecha' };

  useEffect(() => {
    let ignorar = false;
    cargarIncidencias(filtros).then((datos) => { if (!ignorar) setIncidencias(datos); });
    return () => { ignorar = true; };
  }, [filtros]);

  return <PestanaIncidencias incidencias={incidencias} />;
}

Ejercicio 3. El BuscadorBicicletas de CicloUrbano debe cumplir tres requisitos a la vez: (1) el campo responde instantáneamente a cada tecla; (2) no se lanza una petición a json-server por cada tecla; (3) el filtrado local de las 2.000 bicicletas no bloquea el tecleo. Escribe la versión que cumple los tres, usando useDebounce, useDeferredValue y useMemo donde corresponda, y justifica en una tabla qué requisito cubre cada herramienta.

Soluciones

Solución 1.

a) Sobra. Una multiplicación de dos números. El useMemo cuesta más que el cálculo, y el resultado es un primitivo que se compara por valor. Corrección: const total = horas * bicicleta.precioHora;.

b) Es necesario, por partida doble. Ordenar 2.000 elementos es un cálculo de coste real (uso legítimo 1) y el resultado es un array cuya identidad importa para el memo de ListaBicicletas (uso legítimo 2). Además está bien escrito: [...bicicletas] copia antes de ordenar, porque sort muta el array original y mutar los datos de la caché de Query sería un error grave.

c) Sobra. Modal no está memorizado, así que nadie compara alCerrar: se reejecuta igualmente. Corrección: función normal, const manejarCierre = () => setModalAbierto(false);. (Si mañana Modal se envolviera en memo, el useCallback pasaría a ser necesario.)

d) Está mal escrito. useCallback guarda funciones, y aquí se le pasa el resultado de un .filter(): filtradas acaba siendo un array, no una función, y la memorización no hace lo que parece. Corrección:

const filtradas = useMemo(() => bicicletas.filter((b) => b.estado === 'disponible'), [bicicletas]);

e) Está mal escrito, y es el error más peligroso de los cinco. El array de dependencias está vacío, así que valor se congela con el usuario del primer render: cuando alguien inicie sesión, ningún consumidor del contexto se enterará. Un fallo de corrección disfrazado de optimización. Corrección:

const valor = useMemo(
  () => ({ usuario, iniciarSesion, cerrarSesion }),
  [usuario, iniciarSesion, cerrarSesion]
);

Con iniciarSesion y cerrarSesion estabilizadas a su vez con useCallback en el proveedor.

Solución 2. Causa exacta: filtros es un objeto literal creado en cada render. El efecto lo tiene como dependencia, Object.is da false siempre, el efecto se ejecuta, setIncidencias provoca un render, el render crea otro filtros… y el ciclo no termina. Es un bucle de peticiones, no un parpadeo casual.

Solución A, con memorización:

const filtros = useMemo(
  () => ({ estacionId, estado: 'abierta', orden: 'fecha' }),
  [estacionId]
);

Solución B, sin memorización: el objeto no tiene por qué existir fuera del efecto.

useEffect(() => {
  let ignorar = false;
  cargarIncidencias({ estacionId, estado: 'abierta', orden: 'fecha' })
    .then((datos) => { if (!ignorar) setIncidencias(datos); });
  return () => { ignorar = true; };
}, [estacionId]);       // solo un primitivo

Elegiría la B, por tres razones: no añade un hook ni un array de dependencias que mantener, la única dependencia es un primitivo (imposible de romper por identidad) y expresa mejor la intención —«cuando cambie la estación, recarga»—. La regla general: cuando una dependencia inestable rompe un efecto, la primera pregunta es por qué está fuera del efecto.

Y la solución realmente correcta en el CicloUrbano de hoy, después de 07-06: esto no debería ser un useEffect. Es estado del servidor, y le corresponde una consulta con su clave jerárquica:

const { data: incidencias = [] } = useQuery({
  queryKey: claves.estaciones.incidencias(estacionId),
  queryFn: () => cargarIncidencias({ estacionId, estado: 'abierta', orden: 'fecha' })
});

Solución 3.

// src/componentes/BuscadorBicicletas.jsx (versión definitiva)
import { useState, useEffect, useDeferredValue } from 'react';
import { useDispatch } from 'react-redux';
import { useDebounce } from '../hooks/useDebounce.js';
import { terminoCambiado } from '../almacen/sliceCatalogo.js';

function BuscadorBicicletas() {
  const [texto, setTexto] = useState('');            // (1) responde a cada tecla
  const terminoRetrasado = useDebounce(texto, 300);  // (2) una petición por pausa
  const despachar = useDispatch();

  useEffect(() => {
    despachar(terminoCambiado(terminoRetrasado));
  }, [terminoRetrasado, despachar]);

  return (
    <input
      type="search"
      value={texto}
      onChange={(evento) => setTexto(evento.target.value)}
      aria-label="Buscar bicicletas por modelo"
    />
  );
}
// src/paginas/PaginaCatalogo.jsx (fragmento)
const termino = useSelector((estado) => estado.catalogo.termino);
const terminoDiferido = useDeferredValue(termino);       // (3) no bloquea el tecleo
const estaObsoleto = termino !== terminoDiferido;

const visibles = useMemo(                                 // (3) y estabilidad de identidad
  () => filtrarYOrdenar(bicicletas, estaciones, terminoDiferido, tipo, orden),
  [bicicletas, estaciones, terminoDiferido, tipo, orden]
);
Herramienta Requisito que cubre Por qué no sirve otra
useState local en el buscador (1) El campo responde a cada tecla Es estado de interfaz puro; ninguna otra herramienta debe intervenir aquí
useDebounce (2) Una petición por pausa, no por tecla Solo un temporizador evita tráfico de red; ni useDeferredValue ni useTransition reducen peticiones
useDeferredValue (3) El filtrado no bloquea el tecleo Es interrumpible y sin retraso fijo; un segundo useDebounce añadiría latencia percibida
useMemo (3) No repetir el cálculo, y estabilizar visibles El filtrado de 2.000 elementos es caro y su resultado alimenta un componente memorizado
memo en TarjetaBicicleta (3) No reejecutar las tarjetas que no cambian Cierra la cadena: sin él, el array nuevo repinta las 2.000 igual

Conclusión

useMemo(fabrica, dependencias) guarda el valor que produce la fábrica y solo la vuelve a ejecutar cuando alguna dependencia cambia según Object.is; useCallback(funcion, dependencias) es literalmente useMemo(() => funcion, deps) y guarda la función. La fábrica se ejecuta durante el render, así que debe ser pura, y la memorización es una optimización, no una garantía: React puede descartar el valor guardado, y nada de lo que dependa la corrección de tu código puede apoyarse en ella.

Lo que ordena todo lo demás son los dos usos legítimos, que hay que mantener siempre separados. El primero es evitar un cálculo realmente costoso: filtrar, indexar y ordenar 2.000 bicicletas con Intl.Collator, medido antes con console.time y con la CPU ralentizada 4×, con la referencia de los 16 ms del fotograma y la disciplina de quitar el useMemo y volver a medir. Y con la lección de fondo: el Map de estaciones y el colador reutilizado bajan más el coste que el propio useMemo, porque cambian el algoritmo en lugar de cachear el resultado. El segundo uso es mantener la identidad de un valor cuando alguien la compara: un componente envuelto en memo, un array de dependencias de useEffect, el value de un proveedor de contexto o un selector de Redux.

Con eso se han cerrado dos deudas del curso. La de 08-02: ListaBicicletas estabiliza manejarSeleccion y manejarReserva con useCallback([navegar]), y solo entonces el memo de TarjetaBicicleta empieza a servir de algo —los dos juntos, nunca por separado—. Y la de 07-02: ProveedorTema publica un value estabilizado con useMemo y una alternarTema estabilizada con useCallback, y ProveedorAvisos combina esa memorización con la división en contextos de estado y acciones, de modo que un componente que solo llama a mostrarAviso no se reejecuta nunca por los avisos.

Sobre las dependencias, la regla no admite matices: todos los valores reactivos que la fábrica lee, ni uno más ni uno menos. Que sobre una dependencia solo cuesta rendimiento; que falte produce datos obsoletos en pantalla, que es un fallo de corrección. Por eso exhaustive-deps tiene razón casi siempre y silenciarla es la señal de alarma más fiable del ecosistema. Y cuando una dependencia cambia demasiado, hay salidas mejores que insistir: sacar fuera del componente lo que no depende de nada, mover la función dentro del efecto, usar useReducer porque despachar es estable por contrato —igual que setEstado y el dispatch de Redux— y, como último recurso, la referencia siempre al día.

También sabes cuándo no memorizar: primitivos, cálculos triviales, listas cortas, funciones que solo van a un elemento del DOM, props de componentes no memorizados, y el «por si acaso», que es la definición literal de optimización prematura. Y qué hacer en su lugar: bajar el estado, pasar children, sacar constantes fuera, createSelector, select de useQuery, paginar o virtualizar. Por último, los hooks de concurrencia, que atacan el mismo síntoma desde otro ángulo: useTransition marca como interrumpible la actualización que tú provocas —el cambio de tipo o de orden del catálogo—, useDeferredValue difiere el consumo de un valor caro sin retraso fijo, y ninguno sustituye a useDebounce, que sigue siendo la única de las tres que evita peticiones al servidor. En el buscador definitivo conviven las tres. El React Compiler, por su parte, escribe por ti la memorización mecánica y la marca con una insignia ✨ en DevTools, pero no mejora tu algoritmo, no razona sobre valores importados y no decide por ti qué trabajo debería estar ocurriendo.

Con esto, el catálogo de CicloUrbano ya no repite trabajo innecesario cuando el usuario interactúa. Queda el otro problema que 07-06 dejó apuntado y que ninguna memorización toca: que el navegador descarga todo el JavaScript de la aplicación —el taller, el detalle de estaciones, el formulario de reserva, la biblioteca de gráficas— antes de mostrar la primera bicicleta. La próxima lección es División de Código y Carga Perezosa.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados