frontend / react
useMemo et useCallback
Explication
Ce que vous allez apprendre
- Comprendre pourquoi React recalcule tout à chaque rendu par défaut
- Mémoïser une valeur coûteuse à calculer avec
useMemo - Mémoïser une référence de fonction avec
useCallbackpour stabiliser des props - Comprendre pourquoi ces hooks n'ont d'effet réel que combinés à
memo() - Éviter la sur-optimisation prématurée et savoir mesurer avant d'agir
Dans quel contexte ?
Sur src/pages/CatalogPage.jsx, une liste de 5000 produits se re-trie à chaque frappe dans un champ de recherche totalement indépendant, provoquant un lag visible. Le profiler React montre que produits.filter(...).sort(...) s'exécute à chaque rendu du composant, même quand ni produits ni le filtre de tri n'ont changé. Envelopper ce calcul dans useMemo avec les bonnes dépendances élimine le recalcul inutile et fait disparaître le lag.
Étape 1 : ce que React refait à chaque rendu
À chaque nouveau rendu d'un composant, React ré-exécute toute sa fonction : chaque variable est recalculée, chaque fonction interne est recréée depuis zéro.
Étape 2 : quand ça devient un problème
La plupart du temps, ce n'est pas un souci. Mais parfois ce recalcul est réellement coûteux, comme trier une longue liste, ou provoque un effet de bord indésirable ailleurs.
Étape 3 : l'idée de la mémoïsation
La mémoïsation consiste à dire à React : "garde ce résultat en cache, et ne le recalcule que si ses ingrédients ont vraiment changé".
Étape 4 : deux hooks, deux cibles différentes
useMemo mémoïse une VALEUR, le résultat d'un calcul. useCallback mémoïse une FONCTION elle-même, c'est-à-dire sa référence en mémoire.
Étape 5 : le point le plus mal compris
Voici ce qui trompe la plupart des débutants : mémoïser une fonction avec useCallback n'a d'effet visible sur les performances QUE si cette fonction est transmise à un composant enfant enveloppé dans memo().
Étape 6 : pourquoi sans memo() ça ne sert à rien
Sans memo() côté enfant, ce dernier se re-rend de toute façon à chaque rendu du parent, peu importe que la fonction reçue soit "la même" ou non.
Piège fréquent
Envelopper une fonction avec useCallback sans que le composant enfant qui la reçoit soit lui-même enveloppé dans memo() n'apporte strictement aucun gain de performance : l'enfant se re-rend de toute façon.
| Hook | Mémoïse | Utile seul (sans memo) ? |
|---|---|---|
useMemo | Une valeur calculée | Oui, si le calcul est coûteux |
useCallback | Une référence de fonction | Non, seulement combiné à memo() sur l'enfant |
Le piège à éviter
Il reste un dernier piège : mémoïser par réflexe partout ajoute de la complexité pour un gain souvent nul. La bonne pratique est de mesurer avant d'optimiser, avec le Profiler React.
Et ensuite ?
Une fois la mémoïsation comprise, la prochaine étape aborde un problème différent : partager une donnée à travers l'arbre de composants sans la faire transiter manuellement, avec Context.
Commandes & code
useMemo et useCallback
Deux hooks de mémoïsation : l'un pour une VALEUR calculée, l'autre pour une FONCTION.
import { useMemo, useState } from "react";
function ListeFiltree({ produits, filtre }) {
// sans useMemo : ce filtre/tri recalcule à CHAQUE rendu du composant,
// même si "produits" et "filtre" n'ont pas changé (ex: un autre state change ailleurs)
const produitsFiltres = useMemo(() => {
console.log("Recalcul du filtre...");
return produits
.filter((p) => p.nom.toLowerCase().includes(filtre.toLowerCase()))
.sort((a, b) => a.prix - b.prix);
}, [produits, filtre]); // recalculé UNIQUEMENT si produits ou filtre changent
return (
<ul>
{produitsFiltres.map((p) => <li key={p.id}>{p.nom}</li>)}
</ul>
);
}import { useCallback, memo } from "react";
// memo() : le composant enfant ne re-rend que si SES PROPS changent (comparaison superficielle)
const BoutonAction = memo(function BoutonAction({ onClick, label }) {
console.log("Rendu de", label);
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [compte, setCompte] = useState(0);
const [autreState, setAutreState] = useState(0);
// SANS useCallback : une NOUVELLE fonction est créée à CHAQUE rendu du Parent,
// donc memo() sur BoutonAction est inutile -- ses props "changent" toujours
const gererClicMauvais = () => setCompte(compte + 1);
// AVEC useCallback : la MÊME référence de fonction est réutilisée entre rendus,
// tant que les dépendances ne changent pas -- memo() peut alors éviter le re-rendu
const gererClicBon = useCallback(() => {
setCompte((c) => c + 1);
}, []); // pas de dépendance : la forme updater évite d'avoir besoin de "compte" ici
return (
<div>
<p>Compte : {compte}</p>
<button onClick={() => setAutreState(autreState + 1)}>Autre state</button>
<BoutonAction onClick={gererClicBon} label="Incrémenter" />
{/* BoutonAction ne se re-rend PAS quand on clique sur "Autre state" */}
</div>
);
}// Piège : useMemo/useCallback avec une dépendance objet/tableau recréée à chaque rendu
function Composant({ options }) {
// si "options" est un littéral créé à chaque rendu du PARENT ({ tri: "asc" }),
// la dépendance change TOUJOURS même si son contenu est identique -> mémoïsation inutile
const resultat = useMemo(() => calcul(options), [options]);
return <div>{resultat}</div>;
}
// Solution : stabiliser la référence chez le parent, ou dépendre des clés primitives
function Composant2({ tri }) {
const options = useMemo(() => ({ tri }), [tri]); // référence stable tant que "tri" ne change pas
return <SousComposant options={options} />;
}Résumé
useMemomémoïse une VALEUR calculée ;useCallbackmémoïse une RÉFÉRENCE de fonction.- Ces hooks n'ont d'effet visible que combinés à
memo()sur le composant enfant qui reçoit la valeur. - Mémoïser prématurément partout ajoute de la complexité pour un gain souvent négligeable : profiler d'abord.
- Une dépendance objet/tableau recréée à chaque rendu annule l'intérêt de la mémoïsation.
Exercices pratiques
Mission : le useCallback qui n'a jamais rien optimisé
Objectif : Diagnostiquer une mémoïsation inutile faute de memo() côté enfant, et corriger une dépendance objet recréée à chaque rendu.
Contexte
Sur CatalogPage.jsx, un développeur a ajouté useCallback autour de gererClic en pensant accélérer l'affichage, mais le Profiler React montre que BoutonAction (un composant enfant simple, sans memo()) se re-rend quand même à chaque frappe dans le champ de recherche, exactement comme avant. Un peu plus bas, un autre composant fait const options = { tri: "asc" }; directement dans le corps de la fonction, puis useMemo(() => calcul(options), [options]) : le Profiler montre que ce useMemo recalcule à chaque rendu malgré son tableau de dépendances.