La lección anterior estableció que un componente se vuelve a ejecutar por cuatro razones, y que la segunda —un ancestro se ha vuelto a renderizar— es la responsable de la mayor parte del trabajo que sobra en una aplicación React. También estableció que la primera respuesta a ese problema es estructural: bajar el estado, pasar children, paginar. Pero hay casos donde no queda margen estructural: PaginaCatalogo tiene que repintarse cuando cambia el término de búsqueda, y con ella se reejecutan las 2.000 TarjetaBicicleta del catálogo, incluidas las 1.999 cuyos datos son exactamente los mismos que hace un instante. Para eso existe React.memo: una envoltura que le dice a React «antes de ejecutar esta función, compara sus props con las de la última vez; si son iguales, no la ejecutes y reutiliza el resultado anterior». Es una herramienta simple con una API de una sola línea, y aun así es la más mal usada del ecosistema, porque la mitad de los memo que existen en producción no sirven absolutamente de nada. En esta lección verás qué hace exactamente, cómo se aplica al catálogo de CicloUrbano, por qué falla tan a menudo, qué no bloquea nunca y cuánto cuesta ponerlo donde no toca.

Contenido

  1. Qué significa memorizar
  2. React.memo: firma y comportamiento exacto
  3. El árbol de render con y sin memo
  4. Caso práctico: TarjetaBicicleta dentro de ListaBicicletas
  5. Por qué memo a menudo no sirve de nada
  6. Diagnóstico: reproducir el fallo y verlo
  7. El comparador personalizado
  8. Firma invertida respecto a shouldComponentUpdate
  9. Qué bloquea memo y qué no bloquea nunca
  10. Cuándo aplicarlo y cuándo no
  11. El coste de memo
  12. memo y el React Compiler

  1. Qué significa memorizar

Memorizar (del inglés memoization) es guardar el resultado de una operación asociado a sus entradas, para devolver el resultado guardado en lugar de repetir la operación cuando las entradas se repiten. Es una técnica general de programación, y su forma más simple no tiene nada que ver con React:

// Memorización manual de una función pura
function memorizar(funcion) {
  const cache = new Map();

  return function (argumento) {
    if (cache.has(argumento)) {
      return cache.get(argumento);       // no se recalcula nada
    }
    const resultado = funcion(argumento);
    cache.set(argumento, resultado);
    return resultado;
  };
}

const calcularPrecioTotal = memorizar((horas) => horas * 2.5);

calcularPrecioTotal(3);   // calcula: 7.5
calcularPrecioTotal(3);   // devuelve lo guardado, sin calcular

De aquí salen las dos condiciones que hacen que memorizar tenga sentido, y que valen igual para React.memo:

  • La función debe ser pura: mismas entradas, mismo resultado, sin efectos secundarios. Si no lo es, devolver un resultado guardado produce comportamiento incorrecto.
  • Recalcular debe ser más caro que comprobar y guardar. En el ejemplo anterior, horas * 2.5 es más barato que consultar un Map: la memorización lo empeora.

Un componente de React es una función pura de sus props (esa es una de las reglas de 04-04), así que es memorizable por definición. Y la segunda condición —que ejecutarlo sea más caro que comparar sus props— es precisamente lo que hay que verificar antes de aplicar memo, y lo que casi nadie verifica.

  1. React.memo: firma y comportamiento exacto

import { memo } from 'react';

const Componente = memo(function Componente(props) {
  // …
});

memo recibe un componente y devuelve otro componente con el mismo comportamiento más una comprobación previa. La descripción precisa de lo que hace React cuando llega el turno de renderizar un componente envuelto:

  1. Recupera las props del render anterior.
  2. Compara ambos objetos de props superficialmente (shallow): recorre las claves de primer nivel y compara cada valor con Object.is.
  3. Si todas son iguales, no llama a la función del componente y reutiliza el árbol de elementos que produjo la última vez.
  4. Si alguna difiere, ejecuta el componente con normalidad.

Los dos adjetivos de esa comparación son el origen de todos los problemas y de todas las soluciones de esta lección:

Superficial significa un solo nivel de profundidad.

const previas = { bicicleta: { id: 'bici-001', precioHora: 2.5 }, activa: false };
const nuevas  = { bicicleta: { id: 'bici-001', precioHora: 2.5 }, activa: false };

Object.is(previas.activa, nuevas.activa);        // true  → primitivo, mismo valor
Object.is(previas.bicicleta, nuevas.bicicleta);  // false → objetos distintos con igual contenido
// Resultado: memo decide que las props HAN cambiado y renderiza

Por identidad significa Object.is, no igualdad estructural. Para primitivos (string, number, boolean, null, undefined) coincide con lo que esperarías. Para objetos, arrays y funciones, compara la referencia en memoria, no el contenido.

Valor de la prop ¿Object.is da true con el mismo contenido?
'bici-001' ✅ Sí
2.5 ✅ Sí
true ✅ Sí
{ id: 'bici-001' } ❌ No, si es un objeto nuevo
['urbana', 'electrica'] ❌ No, si es un array nuevo
() => reservar(id) ❌ No: cada render crea una función nueva
<EtiquetaEstado /> como prop o children ❌ No: cada render crea un elemento nuevo

Esta tabla es, en la práctica, el índice de la lección: la mitad inferior explica por qué tantos memo no funcionan.

  1. El árbol de render con y sin memo

flowchart TD
    subgraph SIN["SIN memo: se ejecuta todo el subarbol"]
        A1["PaginaCatalogo<br/>cambia el termino"] --> B1["BuscadorBicicletas<br/>se ejecuta"]
        A1 --> C1["ResumenFlota<br/>se ejecuta"]
        A1 --> D1["ListaBicicletas<br/>se ejecuta"]
        D1 --> E1["TarjetaBicicleta x 2.000<br/>se ejecutan TODAS"]
    end
flowchart TD
    subgraph CON["CON memo: React compara antes de bajar"]
        A2["PaginaCatalogo<br/>cambia el termino"] --> B2["BuscadorBicicletas<br/>se ejecuta"]
        A2 --> C2{"ResumenFlota memorizado<br/>props iguales?"}
        C2 -->|Si| C3["NO se ejecuta<br/>se reutiliza el arbol anterior"]
        A2 --> D2["ListaBicicletas<br/>se ejecuta: su prop cambio"]
        D2 --> E2{"TarjetaBicicleta memorizada<br/>props iguales?"}
        E2 -->|"Si, en 1.988 tarjetas"| E3["NO se ejecutan"]
        E2 -->|"No, en 12 tarjetas"| E4["Se ejecutan solo esas"]
    end

El detalle importante del segundo diagrama: cuando memo corta, corta el subárbol entero. React no se limita a saltarse ResumenFlota; se salta también todo lo que ResumenFlota habría renderizado dentro. Por eso el beneficio de memo crece con el tamaño del subárbol que protege, y por eso ponerlo en una hoja barata rara vez compensa.

  1. Caso práctico: TarjetaBicicleta dentro de ListaBicicletas

Este es el estado actual del catálogo de CicloUrbano, con el catálogo ampliado a 2.000 bicicletas.

// src/componentes/TarjetaBicicleta.jsx (versión actual, sin memorizar)
import EtiquetaEstado from './EtiquetaEstado.jsx';
import estilos from './TarjetaBicicleta.module.css';

function TarjetaBicicleta({ bicicleta, nombreEstacion, alSeleccionar, alReservar }) {
  const formateador = new Intl.NumberFormat('es-ES', {
    style: 'currency',
    currency: 'EUR'
  });

  return (
    <article className={estilos.tarjeta}>
      <h3>{bicicleta.modelo}</h3>
      <p className={estilos.estacion}>{nombreEstacion}</p>
      <EtiquetaEstado estado={bicicleta.estado} />
      <p className={estilos.precio}>{formateador.format(bicicleta.precioHora)} / hora</p>

      <button type="button" onClick={() => alSeleccionar(bicicleta.id)}>
        Ver ficha
      </button>
      <button
        type="button"
        onClick={() => alReservar(bicicleta.id)}
        disabled={bicicleta.estado !== 'disponible'}
      >
        Reservar
      </button>
    </article>
  );
}

export default TarjetaBicicleta;

Fíjate en el new Intl.NumberFormat(...) dentro del cuerpo: crear un formateador de Intl es de las operaciones más caras que se pueden meter en un render, del orden de 0,05–0,1 ms cada una. Multiplicado por 2.000 tarjetas son entre 100 y 200 ms por cada pulsación de tecla en el buscador. Este componente no es barato.

Memorizarlo es una línea:

// src/componentes/TarjetaBicicleta.jsx (memorizado)
import { memo } from 'react';
import EtiquetaEstado from './EtiquetaEstado.jsx';
import estilos from './TarjetaBicicleta.module.css';

// El formateador sale del componente: es una constante, no depende de nada
const FORMATO_EURO = new Intl.NumberFormat('es-ES', {
  style: 'currency',
  currency: 'EUR'
});

function TarjetaBicicleta({ bicicleta, nombreEstacion, alSeleccionar, alReservar }) {
  return (
    <article className={estilos.tarjeta}>
      <h3>{bicicleta.modelo}</h3>
      <p className={estilos.estacion}>{nombreEstacion}</p>
      <EtiquetaEstado estado={bicicleta.estado} />
      <p className={estilos.precio}>{FORMATO_EURO.format(bicicleta.precioHora)} / hora</p>

      <button type="button" onClick={() => alSeleccionar(bicicleta.id)}>
        Ver ficha
      </button>
      <button
        type="button"
        onClick={() => alReservar(bicicleta.id)}
        disabled={bicicleta.estado !== 'disponible'}
      >
        Reservar
      </button>
    </article>
  );
}

export default memo(TarjetaBicicleta);

Dos cambios, y el orden en que se han hecho no es casual:

  1. Sacar Intl.NumberFormat fuera del componente. Es la optimización real: elimina 2.000 construcciones de objeto por render sin memorizar nada, y sigue funcionando aunque el memo no acierte nunca. Es la técnica 3 de 08-01 —evitar el trabajo, no acelerarlo— aplicada a una constante.
  2. Envolver la exportación en memo. La convención del proyecto («export default para componentes») se respeta: se memoriza en la exportación y la función conserva su nombre, que es lo que verás en las herramientas de desarrollo y en el Profiler.

Fíjate en el detalle de nombrar la función (function TarjetaBicicleta(...)) en lugar de usar una flecha anónima. Si escribes export default memo(({ bicicleta }) => …), React DevTools mostrará el componente como Anonymous y el Profiler será mucho menos útil.

Y ahora la pregunta que decide todo: con este memo, ¿cuántas tarjetas se saltan el render cuando el usuario escribe una letra en el buscador?

Ninguna. Ni una sola. Vamos a ver por qué.

  1. Por qué memo a menudo no sirve de nada

Este es el componente padre tal y como está escrito hoy en CicloUrbano:

// src/componentes/ListaBicicletas.jsx (el problema)
import { useNavigate } from 'react-router';
import TarjetaBicicleta from './TarjetaBicicleta.jsx';
import estilos from './ListaBicicletas.module.css';

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

  return (
    <ul className={estilos.rejilla}>
      {bicicletas.map((bicicleta) => (
        <li key={bicicleta.id}>
          <TarjetaBicicleta
            bicicleta={bicicleta}
            nombreEstacion={
              estaciones.find((est) => est.id === bicicleta.estacionId)?.nombre ?? '—'
            }
            alSeleccionar={(id) => navegar(`/bicicletas/${id}`)}
            alReservar={(id) => navegar(`/reservas/nueva?bicicleta=${id}`)}
          />
        </li>
      ))}
    </ul>
  );
}

export default ListaBicicletas;

Cada vez que ListaBicicletas se ejecuta, las funciones flecha alSeleccionar y alReservar se crean de nuevo. Son funciones con el mismo código y el mismo comportamiento, pero objetos distintos en memoria. Cuando memo compara:

Object.is(propsPrevias.bicicleta, propsNuevas.bicicleta);           // true  ✅ mismo objeto de la caché de Query
Object.is(propsPrevias.nombreEstacion, propsNuevas.nombreEstacion); // true  ✅ string
Object.is(propsPrevias.alSeleccionar, propsNuevas.alSeleccionar);   // false ❌ función nueva
// → memo concluye que las props han cambiado y renderiza igualmente

Basta una prop con identidad inestable para anular el memo por completo. Y no solo no ahorra nada: ahora, además de ejecutar las 2.000 tarjetas, React ejecuta 2.000 comparaciones de props que siempre fallan. El memo ha empeorado la situación.

Este es el catálogo completo de props que rompen memo, con su forma habitual en CicloUrbano:

Prop inestable Ejemplo real Por qué falla
Función flecha en línea alReservar={(id) => navegar(...)} Función nueva en cada render del padre
Objeto literal estilo={{ margen: 8 }} Objeto nuevo en cada render
style en línea style={{ opacity: 0.5 }} El caso anterior, y el más frecuente de todos
Array literal tipos={['urbana', 'electrica']} Array nuevo en cada render
Resultado de .map/.filter visibles={bicicletas.filter(...)} Array nuevo aunque el contenido sea idéntico
Elemento JSX icono={<EtiquetaEstado estado="libre" />} Elemento nuevo en cada render
children <Panel>{contenido}</Panel> children es una prop más, y casi siempre cambia de identidad
Objeto construido al vuelo usuario={{ id, nombre }} Objeto nuevo aunque id y nombre no cambien

Regla práctica: memo solo funciona si todas las props son primitivos, o son objetos cuya identidad alguien mantiene estable. En CicloUrbano, bicicleta es estable porque viene de la caché de TanStack Query, que devuelve los mismos objetos mientras el dato no se revalide. Las funciones no lo son, y ahí está el fallo.

La solución a este problema no está en esta lección: está en 08-03. useCallback estabiliza la identidad de las funciones y useMemo la de los objetos y arrays. memo y useCallback son dos mitades de la misma herramienta, y usar una sin la otra suele ser trabajo perdido. La lección siguiente cierra este caso con el código definitivo de ListaBicicletas.

  1. Diagnóstico: reproducir el fallo y verlo

Antes de esperar a 08-05, hay una forma inmediata de comprobar si un memo está funcionando, y consiste en instrumentar el componente temporalmente:

// Instrumentación temporal, solo para diagnosticar
function TarjetaBicicleta({ bicicleta, nombreEstacion, alSeleccionar, alReservar }) {
  console.count(`render TarjetaBicicleta ${bicicleta.id}`);
  // …
}

console.count lleva la cuenta por etiqueta. Escribe cinco letras en el buscador con el catálogo de 2.000 bicicletas y mira la consola:

Situación Cuenta esperada tras 5 pulsaciones
Sin memo Cada tarjeta llega a 5 (o a 6 contando el render inicial)
Con memo y props inestables Exactamente igual: el memo no corta nada
Con memo y props estables Cada tarjeta se queda en 1, salvo las que entran o salen del filtro

Un diagnóstico algo más fino, que además dice cuál es la prop culpable:

// src/utilidades/depuracion.js
export function compararPropsConTraza(nombre) {
  return function (propsPrevias, propsNuevas) {
    const claves = new Set([...Object.keys(propsPrevias), ...Object.keys(propsNuevas)]);

    for (const clave of claves) {
      if (!Object.is(propsPrevias[clave], propsNuevas[clave])) {
        console.log(`[${nombre}] cambió la prop "${clave}"`, {
          antes: propsPrevias[clave],
          ahora: propsNuevas[clave]
        });
      }
    }
    return false;   // false = "no son iguales" → deja que renderice; solo observamos
  };
}

// Uso TEMPORAL, nunca en producción:
// export default memo(TarjetaBicicleta, compararPropsConTraza('TarjetaBicicleta'));

Con el ListaBicicletas actual, la consola se llena de cambió la prop "alSeleccionar" y cambió la prop "alReservar", que es el diagnóstico exacto. Recuerda quitarlo después: recorrer las claves de las props de 2.000 componentes en cada render es justo el tipo de coste que este módulo intenta eliminar.

  1. El comparador personalizado

memo acepta un segundo argumento: una función que decide la comparación en tu lugar.

memo(Componente, (propsPrevias, propsNuevas) => boolean)

El contrato es preciso:

  • Devuelve true si consideras que las props son iguales → React no renderiza.
  • Devuelve false si son distintas → React renderiza.

Un uso legítimo en CicloUrbano: TarjetaEstacion recibe el objeto completo de la estación, pero solo pinta el nombre, el barrio y el número de plazas libres. Si el objeto trae además un historial de incidencias que cambia constantemente sin afectar a lo que se ve, la comparación por defecto renderizaría en balde.

// src/componentes/TarjetaEstacion.jsx
import { memo } from 'react';

function TarjetaEstacion({ estacion, plazasLibres, alAbrir }) {
  return (
    <article>
      <h3>{estacion.nombre}</h3>
      <p>{estacion.barrio}</p>
      <p>{plazasLibres} de {estacion.plazas} plazas libres</p>
      <button type="button" onClick={() => alAbrir(estacion.id)}>Ver detalle</button>
    </article>
  );
}

// Solo importan tres campos y la función; el resto del objeto es ruido
function sonEquivalentes(previas, nuevas) {
  return (
    previas.estacion.id === nuevas.estacion.id &&
    previas.estacion.nombre === nuevas.estacion.nombre &&
    previas.estacion.plazas === nuevas.estacion.plazas &&
    previas.plazasLibres === nuevas.plazasLibres &&
    previas.alAbrir === nuevas.alAbrir
  );
}

export default memo(TarjetaEstacion, sonEquivalentes);

Los riesgos, que son serios y explican por qué se usa poco:

  • Es una promesa que tú mantienes. Si mañana la tarjeta empieza a mostrar estacion.barrio y nadie actualiza el comparador, el barrio no se actualizará nunca en pantalla. Es un fallo silencioso, difícil de reproducir y que el linter no detecta.
  • children casi siempre lo invalida. Si el componente recibe children, compararlos correctamente es prácticamente imposible.
  • Comparar en profundidad suele salir más caro que renderizar. Un JSON.stringify(previas) === JSON.stringify(nuevas) sobre un objeto de tamaño medio cuesta más que ejecutar un componente sencillo, y además falla con funciones, fechas y undefined.
// ❌ Antipatrón clásico
export default memo(Tarjeta, (a, b) => JSON.stringify(a) === JSON.stringify(b));

Regla: si necesitas un comparador personalizado, casi siempre el verdadero problema es que el componente recibe props que no necesita. Pasar nombre, barrio y plazasLibres como primitivos en lugar del objeto entero elimina el problema sin comparador y sin memo.

  1. Firma invertida respecto a shouldComponentUpdate

En 04-03, al recorrer el ciclo de vida heredado, apareció shouldComponentUpdate como su antecesor en componentes de clase. La comparación es útil, y su diferencia más peligrosa es el sentido del valor devuelto.

shouldComponentUpdate(nextProps, nextState) memo(C, (propsPrevias, propsNuevas))
Dónde vive Método de una clase Envoltura de una función
Pregunta que responde «¿Debo actualizar?» «¿Son iguales?»
true significa Renderiza No renderices
false significa No renderices Renderiza
Acceso al estado Sí, compara también nextState No: solo props
Versión perezosa PureComponent (comparación superficial) memo sin segundo argumento

La confusión es tan habitual que merece la pena fijarla con una frase: shouldComponentUpdate responde a «actualiza», memo responde a «son iguales». Devolver true en el sitio equivocado produce el peor error posible en una interfaz: un componente que deja de actualizarse, sin ningún mensaje de error, y que solo se descubre cuando alguien nota que el estado de una bicicleta se quedó congelado en disponible.

Y una diferencia de fondo: memo no ve el estado. Lo cual lleva directamente al apartado siguiente.

  1. Qué bloquea memo y qué no bloquea nunca

Este es el apartado que más malentendidos evita. memo interviene en una sola de las cuatro causas de render de 08-01.

Causa del render ¿La bloquea memo? Explicación
Un ancestro se ha renderizado y las props no cambiaron Es exactamente su función, y la única
Un ancestro se ha renderizado y alguna prop cambió de identidad ❌ No La comparación falla y renderiza
Su propio estado cambia (useState, useReducer) Nunca Un componente siempre se renderiza cuando su estado cambia
Un contexto que consume cambia (useContext) Nunca La suscripción al contexto es interna y memo no la ve
Un almacén externo suscrito cambia (useSelector, useQuery) Nunca Igual: la suscripción está dentro del componente
Un key distinto ❌ No Cambiar la clave desmonta y remonta: no hay props previas que comparar

Los dos «nunca» del centro son los importantes, y el del contexto es el que más código inútil ha generado en el mundo:

// ❌ Este memo NO evita nada
const BotonTema = memo(function BotonTema() {
  const { tema, alternarTema } = useTema();   // consume contexto
  return <button onClick={alternarTema}>Tema: {tema}</button>;
});

BotonTema no recibe props, así que la comparación de memo siempre da «iguales»… y aun así el componente se reejecuta cada vez que cambia el valor de ProveedorTema, porque consumir un contexto es una suscripción, no una prop. El memo aquí solo añade una comparación vacía en cada render del padre. Lo que sí funciona para ese problema es lo de 07-02: dividir el contexto en estado y acciones, y estabilizar su value (08-03).

Un matiz que sí es útil, en cambio: memo puede impedir que el componente llegue a reejecutarse por causa del padre, y en ese caso el contexto ni se consulta. Es decir, memo no bloquea la propagación del contexto, pero sí puede reducir cuántas veces se ejecuta el componente por otras causas.

  1. Cuándo aplicarlo y cuándo no

Aplica memo cuando se cumplan las tres condiciones a la vez:

  1. El componente se reejecuta a menudo con las mismas props. Lo has comprobado, no lo supones.
  2. Ejecutarlo es caro: subárbol grande, muchos elementos, cálculos, formateo con Intl, o está repetido cientos de veces en una lista.
  3. Sus props son estables o puedes hacer que lo sean con useMemo/useCallback.

Los casos de CicloUrbano que cumplen las tres:

Componente Por qué compensa
TarjetaBicicleta 2.000 instancias; el padre se repinta con cada tecla del buscador
ResumenFlota Recorre las 2.000 bicicletas para agregar por estado; sus props cambian rara vez
PanelReservas Subárbol grande dentro de una página que se repinta por otros motivos
ListaAvisos Se cuelga bajo el Diseno, que se reejecuta con toda navegación

No apliques memo cuando:

Situación Por qué no
El componente es barato (EtiquetaEstado, IndicadorDeCarga, MigasDePan) Comparar cuesta más que ejecutarlo
Sus props casi siempre cambian La comparación falla siempre: coste puro
Recibe children que se crean en el padre La prop children invalida la comparación casi siempre
Ya está aislado por estructura (recibe children, o su padre no se repinta) El problema ya no existe
Es una página (PaginaCatalogo, PaginaReservas) Se renderiza cuando cambia la ruta, es decir, cuando debe
Se renderiza una vez por pantalla y sin repeticiones No hay nada que ahorrar
Vas a memorizar «por si acaso», sin medir Es la definición de optimización prematura

  1. El coste de memo

memo no es gratis, y su coste tiene tres componentes:

  1. Comparación en cada render del padre. Recorrer las claves de las props y llamar a Object.is por cada una. Es barato, pero multiplicado por 2.000 instancias y por cada pulsación de tecla deja de serlo.
  2. Memoria. React conserva las props anteriores y el árbol de elementos producido. Con 2.000 tarjetas memorizadas, eso es memoria real que no se libera mientras el componente esté montado.
  3. Coste humano. Cada memo es un contrato implícito: «las props de este componente deben mantenerse estables». Quien añada mañana un style={{...}} en línea romperá ese contrato sin enterarse, y el memo quedará como coste sin beneficio, invisible para siempre.

De ahí la conclusión que más código ahorra de toda la lección:

Llenar el proyecto de memo produce una aplicación más lenta, no más rápida. Cada memo que no acierta es comparación y memoria a cambio de nada, y los que no aciertan son la mayoría si nadie ha medido.

Un experimento mental que lo aclara: si envuelves los 26 componentes de CicloUrbano en memo, añades 26 comparaciones por render y unas cuantas decenas de kilobytes de props guardadas. De esos 26, quizá 4 estén en una lista larga o protejan un subárbol caro. Los otros 22 son coste puro. Y ninguno de los 4 funcionará si sus props no son estables.

  1. memo y el React Compiler

Como se adelantó en 08-01, el React Compiler de React 19 aplica esta misma memorización de forma automática. En un proyecto compilado, TarjetaBicicleta se salta el render cuando sus props no han cambiado sin que escribas memo, y —esto es lo verdaderamente relevante— el compilador también estabiliza las funciones flecha que ListaBicicletas crea para cada tarjeta, con lo que el fallo del apartado 5 no llega a producirse.

Qué significa eso en la práctica:

Con el compilador activado Situación
memo explícito en un componente ya optimizado por el compilador Redundante, pero inofensivo: no rompe nada
Componentes que el compilador omite (rompen las reglas de React) El memo manual sigue siendo la única protección
Comparadores personalizados No los sustituye: expresan una decisión que el compilador no puede inferir
Renders por estado propio o por contexto Igual que sin compilador: no los evita
Proyectos sin el compilador (la gran mayoría hoy) Todo lo de esta lección sigue siendo necesario tal cual

Y la razón de fondo por la que esta lección no sobra: cuando algo va mal —una tarjeta que no se actualiza, un render que no se salta— el diagnóstico consiste exactamente en preguntarse qué prop cambió de identidad y por qué. Ese razonamiento es el mismo con compilador y sin él. El compilador te ahorra escribir memo; no te ahorra entenderlo.

Errores Comunes y Consejos

Envolver en memo y dejar las props en línea. Es el error número uno, y produce lo peor de los dos mundos: el mismo número de renders más una comparación por cada uno. Si vas a memorizar, estabiliza las props (08-03) o no memorices.

Creer que memo evita el render por estado o por contexto. No lo hace nunca. Un memo en un componente que consume useTema o useSelector es decoración.

Invertir el comparador personalizado. Devolver true para «ha cambiado» produce un componente congelado. Recuerda: memo pregunta «¿son iguales?».

memo(() => …) con función anónima. El Profiler y las herramientas de desarrollo mostrarán Anonymous, y perderás la mitad de la utilidad de 08-05. Nombra siempre la función.

Memorizar un componente que recibe children. children es una prop, y en el 95 % de los casos es un elemento nuevo en cada render del padre. Si tu componente contenedor recibe children, ya está aislado por estructura (08-01, técnica 2) y no necesita memo.

Consejo: memo es el último recurso, no el primero. Antes: ¿puedo bajar el estado? ¿puedo pasar children? ¿puedo paginar? ¿puedo pasar primitivos en vez del objeto entero? Si alguna respuesta es sí, esa es la solución.

Consejo: pasa primitivos siempre que puedas. <TarjetaBicicleta modelo={b.modelo} estado={b.estado} precioHora={b.precioHora} /> memoriza bien sin ningún esfuerzo, porque los primitivos se comparan por valor. Es más verboso y mucho más robusto.

Consejo: memoriza el contenedor, no las hojas. Un memo en ListaBicicletas protege un subárbol de 2.000 elementos con una sola comparación. Un memo en cada EtiquetaEstado añade 2.000 comparaciones para ahorrar 2.000 <span>. Si puedes cortar arriba, corta arriba.

Ejercicios

Ejercicio 1. Para cada uno de estos cuatro usos de memo en CicloUrbano, indica si el memo funciona, no funciona o es innecesario, y justifica la respuesta en una frase.

// a)
const EtiquetaEstado = memo(function EtiquetaEstado({ estado }) {
  return <span className={estilos[estado]}>{ETIQUETAS[estado]}</span>;
});

// b)
const ResumenFlota = memo(function ResumenFlota({ bicicletas }) {
  const porEstado = bicicletas.reduce((acc, b) => { /* … */ }, {});
  return <dl>{/* … */}</dl>;
});
// Uso: <ResumenFlota bicicletas={data.filter((b) => b.estacionId === estacionId)} />

// c)
const BotonTema = memo(function BotonTema() {
  const { tema, alternarTema } = useTema();
  return <button onClick={alternarTema}>{tema}</button>;
});

// d)
const Panel = memo(function Panel({ titulo, children }) {
  return <section><h2>{titulo}</h2>{children}</section>;
});

Ejercicio 2. PanelReserva está envuelto en memo y aun así se reejecuta en cada render de su padre. Encuentra las tres props que lo impiden y explica qué tipo de valor es cada una. No hace falta que las arregles: eso es 08-03.

function PaginaFichaBicicleta() {
  const { bicicletaId } = useParams();
  const { data: bicicleta } = useBicicleta(bicicletaId);
  const [horas, setHoras] = useState(1);

  return (
    <PanelReserva
      bicicleta={bicicleta}
      horas={horas}
      tarifas={{ hora: bicicleta.precioHora, deposito: 20 }}
      tiposPermitidos={['urbana', 'electrica']}
      alConfirmar={(datos) => crearReserva(datos)}
      estilo={{ marginTop: 16 }}
    />
  );
}

Ejercicio 3. TarjetaBicicleta recibe el objeto bicicleta completo, pero solo usa modelo, estado, precioHora e id. El objeto viene de la caché de TanStack Query y se sustituye entero en cada revalidación (cada 30 segundos por el staleTime del catálogo), aunque los datos sean idénticos. Propón dos soluciones distintas —una con comparador personalizado y otra sin memo en absoluto— y argumenta cuál elegirías.

Soluciones

Solución 1.

Caso Veredicto Justificación
a) EtiquetaEstado Innecesario Recibe un primitivo, así que la comparación acierta, pero el componente es un <span>: comparar cuesta lo mismo o más que ejecutarlo. Coste sin beneficio
b) ResumenFlota No funciona La prop bicicletas es el resultado de un .filter() en el punto de uso: array nuevo en cada render. La comparación falla siempre, y encima el componente es caro. Es el peor escenario posible
c) BotonTema Innecesario No recibe props, así que memo siempre dice «iguales», pero se reejecuta igual cada vez que cambia el contexto de tema. memo no bloquea el contexto
d) Panel No funciona children es una prop y su elemento se crea de nuevo en cada render del padre. Un contenedor con children ya está aislado por estructura y no necesita memo

Solución 2. Tres props rompen la comparación:

  1. tarifas={{ hora: ..., deposito: 20 }}objeto literal: nuevo en cada render aunque precioHora no cambie.
  2. tiposPermitidos={['urbana', 'electrica']}array literal: nuevo en cada render, con contenido constante. Es el caso más absurdo de los tres, porque el valor no depende de nada y podría vivir fuera del componente.
  3. alConfirmar={(datos) => crearReserva(datos)}función flecha en línea: nueva en cada render.
  4. Y una cuarta de regalo: estilo={{ marginTop: 16 }} — objeto literal, el caso más frecuente de todos.

bicicleta sí es estable (viene de la caché de Query) y horas es un número, comparado por valor. Con cuatro props rotas de seis, memo no se salta un solo render: solo añade la comparación.

Solución 3.

Opción A, con comparador personalizado:

function sonEquivalentes(previas, nuevas) {
  return (
    previas.bicicleta.id === nuevas.bicicleta.id &&
    previas.bicicleta.modelo === nuevas.bicicleta.modelo &&
    previas.bicicleta.estado === nuevas.bicicleta.estado &&
    previas.bicicleta.precioHora === nuevas.bicicleta.precioHora &&
    previas.nombreEstacion === nuevas.nombreEstacion &&
    previas.alSeleccionar === nuevas.alSeleccionar &&
    previas.alReservar === nuevas.alReservar
  );
}

export default memo(TarjetaBicicleta, sonEquivalentes);

Funciona, pero crea una deuda de mantenimiento: el día que la tarjeta muestre bicicleta.tipo, ese campo dejará de actualizarse en pantalla y nadie recibirá ningún aviso.

Opción B, sin memo: pasar primitivos.

<TarjetaBicicleta
  id={bicicleta.id}
  modelo={bicicleta.modelo}
  estado={bicicleta.estado}
  precioHora={bicicleta.precioHora}
  nombreEstacion={nombreEstacion}
  alSeleccionar={alSeleccionar}
  alReservar={alReservar}
/>

Con props primitivas, la comparación superficial por defecto de memo acierta siempre, sin comparador y sin deuda: si el objeto se sustituye pero modelo, estado y precioHora valen lo mismo, Object.is da true en las tres. Además, el contrato del componente queda explícito: se ve de un vistazo qué necesita.

Cuál elegir: la opción B. Es más robusta, se autodocumenta y no puede quedarse desactualizada. El único argumento a favor de A es la comodidad de pasar un objeto, y esa comodidad se paga con un fallo silencioso. La regla general: cuando un comparador personalizado parece necesario, revisa primero el contrato de props.

Conclusión

React.memo hace una sola cosa y la hace bien: envuelve un componente y, antes de ejecutarlo, compara sus props por identidad y de forma superficial con las del render anterior; si son todas iguales, se salta la ejecución y reutiliza el árbol de elementos que produjo la última vez, cortando además todo el subárbol que colgaba de él. Aplicado a TarjetaBicicleta dentro de un catálogo de 2.000 elementos, el potencial es enorme —sobre todo tras sacar el Intl.NumberFormat fuera del componente, que es la optimización que funciona con memo y sin él.

Pero el potencial no se materializa solo. memo compara por identidad, así que basta una función flecha en línea, un objeto literal, un style={{}}, un array construido al vuelo o un children para que la comparación falle siempre y el memo se convierta en puro coste. Es exactamente lo que pasa hoy en ListaBicicletas con alSeleccionar y alReservar, y sabes diagnosticarlo con console.count y con un comparador de traza que señala la prop culpable. La mitad que falta —useMemo y useCallback para estabilizar esas identidades— es la lección siguiente, y hasta entonces el memo del catálogo no ahorra un solo render.

También has visto los límites. El comparador personalizado (propsPrevias, propsNuevas) => boolean tiene la firma invertida respecto a shouldComponentUpdate: aquí true significa «son iguales, no renderices», y confundirlo produce componentes congelados sin ningún mensaje de error; comparar en profundidad casi siempre sale más caro que renderizar, y cuando un comparador parece necesario, lo que suele fallar es el contrato de props. Y sobre todo: memo interviene en una sola de las cuatro causas de render. No bloquea nunca el render por estado propio, ni por contexto consumido, ni por un almacén externo suscrito. Un memo sobre BotonTema es decoración.

De ahí el criterio: memoriza cuando se cumplan las tres condiciones —se reejecuta a menudo con las mismas props, ejecutarlo es caro, y sus props son o pueden hacerse estables— y no memorices componentes baratos, props volátiles, contenedores con children ni subárboles ya aislados por estructura. Cada memo cuesta una comparación por render, memoria y un contrato implícito que el siguiente desarrollador romperá sin darse cuenta: llenar el proyecto de memo produce una aplicación más lenta. El React Compiler automatiza esta memorización cuando está activado, pero no sustituye a los comparadores personalizados, no evita los renders por estado o contexto, omite el código que rompe las reglas de React, y no te ahorra el razonamiento —qué prop cambió de identidad y por qué— que es justo el que hace falta cuando algo va mal.

Queda pendiente la deuda más citada del módulo: cómo conseguir que alSeleccionar sea la misma función entre renders, que tarifas sea el mismo objeto, que el value de un proveedor de contexto no cambie de identidad sin motivo (la deuda abierta en 07-02) y que ordenar 2.000 bicicletas no se repita cuando nada ha cambiado. Todo eso son dos hooks. La próxima lección es Hooks useMemo y useCallback.

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