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
- Qué significa memorizar
React.memo: firma y comportamiento exacto- El árbol de render con y sin
memo - Caso práctico:
TarjetaBicicletadentro deListaBicicletas - Por qué
memoa menudo no sirve de nada - Diagnóstico: reproducir el fallo y verlo
- El comparador personalizado
- Firma invertida respecto a
shouldComponentUpdate - Qué bloquea
memoy qué no bloquea nunca - Cuándo aplicarlo y cuándo no
- El coste de
memo memoy el React Compiler
- 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 calcularDe 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.5es más barato que consultar unMap: 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.
React.memo: firma y comportamiento exacto
React.memo: firma y comportamiento exactomemo 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:
- Recupera las props del render anterior.
- Compara ambos objetos de props superficialmente (shallow): recorre las claves de primer nivel y compara cada valor con
Object.is. - Si todas son iguales, no llama a la función del componente y reutiliza el árbol de elementos que produjo la última vez.
- 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 renderizaPor 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.
- El árbol de render con y sin
memo
memoflowchart 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.
- Caso práctico:
TarjetaBicicleta dentro de ListaBicicletas
TarjetaBicicleta dentro de ListaBicicletasEste 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:
- Sacar
Intl.NumberFormatfuera del componente. Es la optimización real: elimina 2.000 construcciones de objeto por render sin memorizar nada, y sigue funcionando aunque elmemono acierte nunca. Es la técnica 3 de 08-01 —evitar el trabajo, no acelerarlo— aplicada a una constante. - Envolver la exportación en
memo. La convención del proyecto («export defaultpara 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 escribesexport default memo(({ bicicleta }) => …), React DevTools mostrará el componente comoAnonymousy 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é.
- Por qué
memo a menudo no sirve de nada
memo a menudo no sirve de nadaEste 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 igualmenteBasta 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:
memosolo funciona si todas las props son primitivos, o son objetos cuya identidad alguien mantiene estable. En CicloUrbano,bicicletaes 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.
- 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.
- El comparador personalizado
memo acepta un segundo argumento: una función que decide la comparación en tu lugar.
El contrato es preciso:
- Devuelve
truesi consideras que las props son iguales → React no renderiza. - Devuelve
falsesi 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.barrioy 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. childrencasi siempre lo invalida. Si el componente recibechildren, 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 yundefined.
// ❌ 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,barrioyplazasLibrescomo primitivos en lugar del objeto entero elimina el problema sin comparador y sinmemo.
- Firma invertida respecto a
shouldComponentUpdate
shouldComponentUpdateEn 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.
- Qué bloquea
memo y qué no bloquea nunca
memo y qué no bloquea nuncaEste 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 | ✅ Sí | 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 sí 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.
- Cuándo aplicarlo y cuándo no
Aplica memo cuando se cumplan las tres condiciones a la vez:
- El componente se reejecuta a menudo con las mismas props. Lo has comprobado, no lo supones.
- Ejecutarlo es caro: subárbol grande, muchos elementos, cálculos, formateo con
Intl, o está repetido cientos de veces en una lista. - 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 |
- El coste de
memo
memomemo no es gratis, y su coste tiene tres componentes:
- Comparación en cada render del padre. Recorrer las claves de las props y llamar a
Object.ispor cada una. Es barato, pero multiplicado por 2.000 instancias y por cada pulsación de tecla deja de serlo. - 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.
- Coste humano. Cada
memoes un contrato implícito: «las props de este componente deben mantenerse estables». Quien añada mañana unstyle={{...}}en línea romperá ese contrato sin enterarse, y elmemoquedará 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
memoproduce una aplicación más lenta, no más rápida. Cadamemoque 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.
memo y el React Compiler
memo y el React CompilerComo 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:
tarifas={{ hora: ..., deposito: 20 }}— objeto literal: nuevo en cada render aunqueprecioHorano cambie.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.alConfirmar={(datos) => crearReserva(datos)}— función flecha en línea: nueva en cada render.- 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
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
