Inicio / Artículos

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.

Editor: REACTAPP.TOP5 min de lectura1092 palabras

Fuentes: React.memo: Useful, but Often Misused

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

  • memo solo es útil mientras cada prop mantenga una referencia estable.
  • Las funciones, objetos, arrays e valores de contexto inline suelen anular su efecto.
  • useCallback sin un hijo memorizado es un esfuerzo inútil.
  • Reserve useMemo para cálculos costosos o referencias que deben permanecer estables.
  • La virtualización de listas largas y la división de componentes grandes suelen ser más útiles que la memorización.
  • Analice primero el rendimiento; deje que los datos tomen la decisión.
  • Lecturas relacionadas