Retour au cours

frontend / react

useMemo et useCallback

Leçon 91 exercice

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 useCallback pour 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.

HookMémoïseUtile seul (sans memo) ?
useMemoUne valeur calculéeOui, si le calcul est coûteux
useCallbackUne référence de fonctionNon, 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.

jsx
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>
    );
}
jsx
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>
    );
}
jsx
// 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é

  • useMemo mémoïse une VALEUR calculée ; useCallback mé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

1 disponible
1

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.

Résoudre l’exercice →