Por qué React.memo sigue re-renderizando: la identidad de referencia en los props
Vea cómo React.memo compara los props, por qué las funciones inline, los objetos y los arrays lo invalidan, cómo estabilizar las referencias, y por qué el perfilamiento debe realizarse antes de la memorización.
React.memo a menudo se considera como un interruptor de rendimiento: al envolver un componente, las actualizaciones repetidas desaparecen. En la práctica, las aplicaciones llenas de memo aún pueden sentirse lentas, ya que las interfaces de usuario de React lentas suelen deberse a la estructura del componente, propiedades inestables, cálculos costosos o simplemente a demasiadas actualizaciones de renderizado, y no a la falta de un envoltorio. Esta guía muestra qué es lo que realmente compara memo, los patrones de propiedades que lo invalidan en silencio, y cómo decidir si realmente vale la pena usarlo.
Qué verifica realmente memo
memo no impide que un componente se renderice. Compara las nuevas propiedades con las anteriores, una por una, utilizando Object.is, y solo omite el renderizado cuando todas son iguales. Los cambios en el estado propio del componente o en un contexto que este lee siguen haciendo que se renderice. Las propiedades primitivas como cadenas de texto, números y valores booleanos se comparan por su valor, por lo que funcionan de forma natural.
import { memo } from "react";
const App = () => {
const name = "John";
const age = 25;
return <UserCard name={name} age={age} />
};
const UserCard = memo(({ name, age }) => {
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
</div>
);
});
Si name y age no cambian, React puede reutilizar el resultado anterior. Los problemas surgen con funciones, objetos y arrays, ya que en estos casos Object.is compara las referencias y no su contenido.
Las callbacks inline se crean de nuevo en cada renderizado
Aquí un padre pasa una función flecha a un botón memorizado.
import React, { useState, memo } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<h2>Count: {count}</h2>
<CounterButton
onIncrement={() => setCount((prev) => prev + 1)}
/>
</div>
);
};
const CounterButton = memo(({ onIncrement }) => {
return (
<button onClick={onIncrement}>
Increment
</button>
);
});
Cada renderizado de App crea una función nueva. Su cuerpo es idéntico, pero la referencia difiere, por lo que memo considera que hay un cambio en la propiedad y vuelve a renderizar CounterButton cada vez.
La solución es mantener estable la referencia con useCallback. Dado que se utiliza la forma de actualización prev => prev + 1, el array de dependencias puede permanecer vacío.
import React, { useState, useCallback } from "react";
const App = () => {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<h2>Count: {count}</h2>
<CounterButton
onIncrement={handleIncrement}
/>
</div>
);
};
const CounterButton = ({ onIncrement }) => {
return (
<button onClick={onIncrement}>
Increment
</button>
);
};
Obsérvese que en este fragmento CounterButton ya no está envuelto en memo. Sin este envoltorio, el hijo se vuelve a renderizar junto con su padre sin importar nada, y useCallback no sirve de nada. Ambos solo son útiles juntos: mantener memo(...) alrededor del hijo del ejemplo anterior. Además, no se debe añadir useCallback en todas partes; tiene su propio costo y solo debe usarse donde resuelva un problema concreto.
Los objetos y arrays en línea se comportan de la misma manera
Un literal de objeto dentro del cuerpo del componente se recrea en cada renderizado.
import { memo } from "react";
const App = () => {
const config = {
theme: "dark",
pageSize: 20,
showFilters: true,
};
return (
<div>
<DataGrid config={config} />
</div>
);
};
const DataGrid = memo(({ config }) => {
return (
<div>
<h3>Theme: {config.theme}</h3>
<p>Page Size: {config.pageSize}</p>
</div>
);
});
Lo mismo ocurre con un array de opciones.
import { memo } from "react";
const App = () => {
const options = [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "User", value: "user" },
];
return (
<div>
<Select options={options} />
</div>
);
};
const Select = memo(({ options }) => {
return (
<select>
{options.map((option) => (
<option key={option.value}>
{option.label}
</option>
))}
</select>
);
});
Ambos generan referencias nuevas cada vez, por lo que sus hijos memorizados se vuelven a renderizar.
Subir datos que nunca cambian
Cuando los datos son estáticos, muévalos al ámbito del módulo para que se creen una sola vez. La lista de opciones puede estar fuera del componente.
import { useState, useMemo, memo } from "react";
const options = [
{ label: role.toUpperCase(), value: role },
{ label: "Editor", value: "editor" },
{ label: "User", value: "user" },
]
const App = () => {
return (
<div>
<Select options={options} />
</div>
);
};
const Select = memo(({ options }) => {
return (
<select>
{options.map((option) => (
<option key={option.value}>
{option.label}
</option>
))}
</select>
);
});
export default App;
Tal como está impreso, la primera entrada hace referencia a una variable role que no existe en el ámbito del módulo, por lo que este código lanza un ReferenceError. Utilice un literal como { label: "Admin", value: "admin" }. Si una entrada realmente depende de props o estado, mantenga el array dentro del componente y envuélvalo en useMemo con esos valores como dependencias (lo cual presumiblemente es la razón por la que se importa useMemo).
El objeto de configuración recibe el mismo tratamiento.
import { memo } from "react";
const config = {
theme: "dark",
pageSize: 20,
showFilters: true,
};
const App = () => {
return (
<div>
<DataGrid config={config} />
</div>
);
};
const DataGrid = memo(({ config }) => {
return (
<div>
<h3>Theme: {config.theme}</h3>
<p>Page Size: {config.pageSize}</p>
</div>
);
});
Mida antes de memorizar
Las preguntas útiles surgen antes de cualquier análisis: qué componente es realmente lento, por qué se renderiza, si ese proceso de renderizado es costoso y si se puede reducir el trabajo en lugar de ocultarlo. El Profilador de React DevTools responde a la mayoría de ellas, mostrando qué componentes se renderizaron, por qué, cuánto tiempo tardó cada uno y qué actualizaciones fueron innecesarias. La lista de verificación en hacer menos trabajo antes de la memorización amplía ese orden de operaciones.
Puntos clave
memosolo es útil mientras cada prop mantenga una referencia estable.- Las funciones, objetos, arrays e valores de contexto inline suelen anular su efecto.
useCallbacksin un hijo memorizado es un esfuerzo inútil.- Reserve
useMemopara cálculos costosos o referencias que deben permanecer estables.
Lecturas relacionadas
- Un listado de revisión de código en React: State derivada, Reductores, Effects y Memo — Aprenda a identificar siete problemas comunes en las revisiones de código en React, desde estado duplicado y obtención de datos manual hasta uso incorrecto de Effects y memorización excesiva, y qué escribir en su lugar.
- Las claves en React como identidad de componente: Preservando y restableciendo el estado — Entienda por qué las claves en React determinan qué instancia de componente sobrevive a un render, cómo las claves de índice causan fugas de estado entre filas y cómo utilizar una clave para restablecer un formulario.