Pourquoi React.memo continue de rérender : l’identité de référence dans les props
Voyez comment React.memo compare les props, pourquoi les fonctions en ligne, les objets et les tableaux le rendent inefficace, comment stabiliser les références, et pourquoi l’analyse des performances doit précéder la mise en mémoire.
React.memo est souvent considéré comme un outil pour améliorer les performances : en l’appliquant à un composant, les re-render disparaissent. En pratique, les applications remplies de memo peuvent encore sembler lentes, car des interfaces React lentes proviennent généralement de la structure des composants, de props instables, de calculs coûteux ou simplement d’un rendu excessif, et non du manque d’enveloppe. Ce guide montre ce que memo compare réellement, les schémas de props qui l’annulent silencieusement, et comment déterminer s’il vaut la peine d’être utilisé.
Ce que memo vérifie réellement
memo n’empêche pas un composant de s’afficher. Il compare les nouvelles propriétés avec les précédentes, une par une, en utilisant Object.is, et ne saute l’affichage que lorsque toutes sont identiques. Les modifications de l’état propre du composant ou d’un contexte qu’il lit entraînent néanmoins son affichage. Les propriétés primitives telles que les chaînes de caractères, les nombres et les booléens sont comparées par valeur, ce qui fonctionne naturellement.
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 et age restent inchangés, React peut réutiliser le résultat précédent. Les problèmes apparaissent avec les fonctions, les objets et les tableaux, car pour ceux-ci Object.is compare les références plutôt que le contenu.
Les fonctions d’appel en ligne sont créées à chaque affichage
Ici, un parent transmet une fonction flèche à un bouton mémorisé.
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>
);
});
Chaque rendu de App crée une nouvelle fonction. Son corps est identique, mais la référence diffère, ce qui fait que memo perçoit une propriété modifiée et rend CounterButton à chaque fois.
La solution consiste à maintenir une référence stable en utilisant useCallback. Comme la forme de mise à jour prev => prev + 1 est utilisée, le tableau de dépendances peut rester vide.
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>
);
};
Remarquez que dans ce fragment CounterButton n’est plus entouré de memo. Sans cet enrobage, l’élément enfant se re-rend avec son parent quel que soit le cas, et useCallback ne sert à rien. Ces deux éléments ne sont utiles qu’ensemble : conservez memo(...) autour de l’élément enfant de l’exemple précédent. Et n’ajoutez pas useCallback partout ; il a ses propres coûts et doit être utilisé là où il résout un problème concret.
Les objets en ligne et les tableaux se comportent de la même manière
Un objet littéral à l’intérieur du corps du composant est recréé à chaque rendu.
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>
);
});
Il en va de même pour un tableau d’options.
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>
);
});
Les deux génèrent de nouvelles références à chaque fois, ce qui fait que leurs enfants mémorisés sont rendus à nouveau.
Faire monter en haut les données qui ne changent jamais
Lorsque les données sont statiques, déplacez-les dans le scope du module afin qu’elles soient créées une seule fois. La liste des options peut se trouver en dehors du composant.
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;
Au moment de l’affichage, la première entrée fait référence à une variable role qui n’existe pas au niveau du module, ce qui provoque une erreur ReferenceError. Utilisez un littéral tel que { label: "Admin", value: "admin" }. Si une entrée dépend réellement de props ou d’état, conservez le tableau à l’intérieur du composant et enveloppez-le dans useMemo en indiquant ces valeurs comme dépendances (ce qui explique probablement pourquoi useMemo est importé).
L’objet de configuration subit le même traitement.
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>
);
});
Mesurez avant de mettre en mémoire
Avant toute analyse, il convient de se poser des questions utiles : quel composant est réellement lent, pourquoi s’il rend, si ce rendu est coûteux, et si l’on peut réduire les tâches plutôt que de les masquer. Le Profilleur des React DevTools répond à la plupart d’entre elles, en indiquant quels composants se sont rendus, pourquoi, combien de temps cela a pris et quelles mises à jour étaient inutiles. La liste de contrôle sur faire moins de travail avant la mémorisation développe cette séquence d’opérations.
Points clés
memone fonctionne que tant que chaque prop conserve une référence stable.- Les fonctions, objets, tableaux et valeurs de contexte en ligne annulent fréquemment son effet.
useCallbacksans enfant mémorisé représente un effort inutile.- Réservez
useMemoaux calculs coûteux ou aux références qui doivent rester stables.