Module 5 · React

useContext, useMemo, useCallback, memo

Voici les quatre outils qui font peur sur le papier et qui, une fois compris, deviennent presque évidents. Ils ne sont pas là pour rendre ta vie compliquée : ils répondent tous à des problèmes très précis du modèle « ma fonction-composant est rappelée en boucle » qu'on a posé dans le module sur le modèle mental de React. La règle du jeu aujourd'hui : on prend le temps de comprendre le pourquoi profond avant le comment. Une fois le problème vraiment vu, le hook se lit tout seul.

💡 Le fil rouge de tout le module

Rappelle-toi la grande idée du module sur le modèle mental de React : React rappelle ta fonction-composant à chaque changement. Ce simple fait, qui est la force de React (« l'UI est une fonction de l'état »), crée mécaniquement deux douleurs. Tout ce module ne fait que répondre à ces deux douleurs :

  • Faire descendre une donnée jusqu'à un composant très imbriqué oblige à la passer de main en main à chaque étage. Pénible. → useContext.
  • Comme la fonction re-tourne en entier à chaque rendu, elle recalcule tout et recrée tous ses objets et fonctions, même quand rien d'utile n'a bougé. → useMemo, useCallback et memo évitent ce gaspillage quand il fait vraiment mal.

Garde cette boussole : ces hooks ne sont pas « le bon réflexe à avoir partout », ce sont des remèdes à des symptômes. Pas de symptôme, pas de remède.

1. L'égalité référentielle — la vérité cachée derrière tout

Avant même de parler de React, il faut comprendre une bizarrerie de JavaScript qui est, à elle seule, la clé de tout ce module. Si tu retiens une seule section aujourd'hui, c'est celle-ci. Quand tu auras compris pourquoi {} !== {}, la moitié de React qui te paraissait magique deviendra logique.

En JavaScript, il existe deux familles de valeurs, et elles ne se comparent pas du tout de la même façon. D'un côté, les valeurs primitives : nombres, chaînes, booléens. De l'autre, les objets : les objets au sens large, donc aussi les tableaux et les fonctions (oui, une fonction est un objet en JavaScript).

Les primitives se comparent par leur contenu. C'est intuitif :

// Deux primitives identiques SONT égales. Logique, rassurant.
2 === 2            // true
'rouge' === 'rouge' // true
true === true       // true

Les objets, eux, se comparent par leur identité — autrement dit par leur référence, leur « adresse en mémoire ». Deux objets qui ont exactement le même contenu mais qui ont été créés séparément sont considérés comme différents. C'est ici que ça devient déroutant la première fois :

// Même contenu visible... et pourtant DIFFÉRENTS.
{} === {}              // false  ← deux objets vides, mais créés séparément
[] === []              // false  ← deux tableaux vides, idem
[1, 2] === [1, 2]      // false  ← même contenu, références différentes
(() => {}) === (() => {}) // false  ← deux fonctions identiques au code près

// La SEULE façon d'avoir true : que ce soit le MÊME objet.
const a = { couleur: 'rouge' };
const b = a;           // b pointe vers le même objet que a
a === b                // true   ← même référence, pas une copie
💡 L'analogie de l'adresse postale

Imagine deux maisons construites sur le même plan, meublées à l'identique, au papier peint près. Sont-elles « la même maison » ? Non : elles ont deux adresses différentes. Tu peux repeindre l'une sans toucher l'autre. Un objet JavaScript, c'est l'adresse. === sur deux objets ne demande pas « est-ce le même décor ? » mais « est-ce la même adresse ? ». Pour les primitives au contraire, il n'y a pas d'adresse : le nombre 2 est 2, point. C'est pour ça que 2 === 2 est vrai mais que {} === {} est faux.

« D'accord, mais en quoi ça concerne React ? » Énormément. Souviens-toi du module sur le modèle mental de React : à chaque rendu, la fonction-composant re-tourne en entier. Donc chaque ligne qui écrit un objet, un tableau ou une fonction crée une toute nouvelle référence à chaque rendu :

function Ecran() {
  // À CHAQUE rendu, ces trois lignes fabriquent des objets/fonctions NEUFS.
  const style = { padding: 16 };          // nouvel objet à chaque rendu
  const ids = [1, 2, 3];                   // nouveau tableau à chaque rendu
  const onPress = () => console.log('go'); // nouvelle fonction à chaque rendu

  // style, ids, onPress sont "les mêmes pour toi"... mais pour === ils sont
  // DIFFÉRENTS d'un rendu à l'autre. C'est exactement ce qui "casse" memo.
  return /* ... */;
}

Retiens cette phrase comme un mantra : « même contenu » n'est pas « même référence ». React, quand il compare des props ou des dépendances, regarde la référence, pas le contenu. Tout le reste du module — pourquoi memo ne marche pas tout seul, pourquoi useMemo et useCallback existent, pourquoi les tableaux de dépendances comptent — découle directement de ce seul fait. On va y revenir sans cesse.

🧭 Pourquoi React compare les références et pas le contenu

Par paresse intelligente. Comparer deux références, c'est comparer deux adresses : une opération instantanée. Comparer le contenu de deux objets potentiellement énormes et imbriqués, ce serait lent — parfois plus lent que de simplement refaire le travail qu'on essayait d'éviter. React a donc choisi la comparaison la moins chère qui existe. C'est le bon choix par défaut, mais il nous oblige, nous, à stabiliser nos références quand on veut que React « voie » que rien n'a changé. D'où les hooks de ce module.

2. Le vrai coût d'un re-render — c'est cher quand ?

Avant de dégainer la moindre optimisation, il faut une intuition juste de ce qui coûte cher et de ce qui ne coûte rien. Beaucoup de débutants « optimisent » des choses gratuites et alourdissent leur code pour zéro gain. Posons les choses calmement.

Un re-render, ce n'est pas « redessiner l'écran ». C'est : React rappelle ta fonction-composant, obtient une nouvelle description de l'UI (l'arbre de React elements), puis la compare à la précédente et n'applique au vrai écran que les différences. Cette étape de comparaison s'appelle la réconciliation, et React la fait extrêmement vite. Re-render un petit composant qui retourne trois lignes de JSX, c'est de l'ordre de quelques microsecondes. Autant dire gratuit.

Alors, qu'est-ce qui est réellement coûteux ? Trois grandes familles :

  • Les calculs lourds dans le corps du composant. Filtrer ou trier un tableau de plusieurs milliers d'éléments, faire des maths sur de grosses structures, sérialiser quelque chose de volumineux. Si ce calcul se refait à chaque rendu sans raison, c'est du gâchis pur.
  • Les grandes listes et les arbres profonds. Re-render un composant, c'est pas cher ; re-render des centaines de composants enfants parce que le parent a bougé, ça commence à se sentir. Un re-render de 5 nœuds : invisible. De 2000 nœuds, dont certains font des calculs : là, on peut voir le chrono d'un entraînement saccader.
  • Les re-renders très fréquents. Un composant qui se redessine une fois sur un clic, on s'en moque même s'il est un peu lourd. Le même composant qui se redessine chaque seconde à cause d'un chrono, ou à chaque pixel d'un défilement, change tout : un petit coût répété 60 fois par minute devient un vrai coût.

La règle qui en découle est simple à énoncer : fréquence × coût unitaire = douleur. Un calcul cher mais rare ? On laisse. Un calcul léger mais répété 60 fois par seconde ? Souvent on laisse aussi. Un calcul cher et fréquent, sur une grosse structure ? Là, oui, on mémoïse. Tu remarqueras que l'exemple réel d'Halterofit qu'on verra plus bas coche pile cette case : un filtrage de liste qui, sans précaution, se referait à chaque tick du chrono.

🧭 Quand l'optimisation NUIT

Mémoïser n'est pas gratuit. useMemo et useCallback doivent eux aussi stocker l'ancienne valeur, garder le tableau de dépendances, le comparer à chaque rendu. Sur un calcul déjà trivial, ce « coût de la mémoïsation » peut être supérieur au calcul qu'on voulait éviter — tu paies pour ne rien gagner. Et il y a un coût caché tout aussi réel : la lisibilité. Chaque useMemo est une ligne de bruit de plus, un tableau de dépendances de plus à tenir à jour, une source de bugs de plus si tu oublies une dépendance. Le code le plus rapide à lire reste le code simple. On n'optimise pas par superstition : on optimise un problème qu'on a vu ou mesuré.

3. useContext — partager sans « faire passer la prop » partout

💡 Le problème : le « prop drilling »

Imagine un bouton qui doit dire à son texte « sois blanc ». Si le texte est enfoui à trois niveaux de profondeur, il faudrait passer la prop color à chaque niveau intermédiaire, juste pour la transmettre — alors qu'aucun de ces niveaux ne s'en sert lui-même. C'est le prop drilling (le « forage de props ») : fastidieux, verbeux, et fragile parce qu'un seul étage qui oublie de transmettre casse la chaîne. Le Contexte règle ça en créant un « canal » : un parent y dépose une valeur, et n'importe quel descendant, aussi loin soit-il, la lit directement sans intermédiaire.

Ton app utilise exactement ce mécanisme pour la couleur du texte des boutons. Deux fichiers se parlent à travers un contexte, sans jamais se passer de prop de couleur. Regardons les deux bouts du canal.

Étape A — on crée le canal, et le composant Text le lit :

apps/mobile/src/components/ui/text.tsx
// On crée le canal. La valeur par défaut est undefined : si personne n'a
// rien déposé au-dessus, le Text n'a aucune couleur imposée par le contexte.
const TextClassContext = React.createContext<string | undefined>(undefined);

function Text({ className, variant = 'default', ...props }) {
  // On LIT ce que le canal contient, ici et maintenant, à ce point de l'arbre.
  // Si un parent a déposé une classe de couleur plus haut, on la récupère.
  const textClass = React.useContext(TextClassContext);

  return (
    <Component
      // textClass (venu du contexte) est fusionné avec les autres classes.
      className={cn(textVariants({ variant }), textClass, className)}
      {...props}
    />
  );
}

Étape B — le Button DÉPOSE une valeur dans le canal pour tous ses enfants :

apps/mobile/src/components/ui/button.tsx
function Button({ className, variant, size, ...props }) {
  return (
    // .Provider = "je dépose CETTE valeur dans le canal pour tout ce qui est
    // à l'intérieur". value = la bonne classe de couleur selon la variante
    // (default, destructive, outline...) calculée par buttonTextVariants.
    <TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
      <Pressable className={cn(/* ... */)} role="button" {...props} />
    </TextClassContext.Provider>
  );
}

Résultat presque magique : tu écris <Button><Text>Valider</Text></Button> et le Text prend automatiquement la bonne couleur, sans que tu lui passes la moindre prop de couleur. Le Button l'a déposée dans le canal, le Text l'a lue. C'est ça, useContext : lire une valeur fournie plus haut dans l'arbre, peu importe la distance.

Maintenant, un point que beaucoup ignorent et qui se relie directement à la section sur l'égalité référentielle. Le Contexte a un comportement de re-render à connaître :

💡 Un changement de valeur re-render TOUS les consommateurs

Quand la value d'un Provider change, tous les composants qui lisent ce contexte avec useContext re-render — même ceux qui ne se servent que d'un petit morceau de la valeur. Et « change », pour React, ça veut dire change de référence (revoici la section 1 !). Donc si tu déposes un objet fabriqué à la volée, par exemple value={{ user, theme }}, tu crées une nouvelle référence à chaque rendu du Provider, et tu fais re-render tous les consommateurs à chaque fois, pour rien. La parade classique : stabiliser cet objet avec useMemo (section 5). Tu vois comme tout se tient déjà.

De ce comportement découle une bonne habitude d'architecture : découper les contextes. Plutôt qu'un énorme contexte qui contient tout (l'utilisateur, le thème, les préférences, l'état du formulaire...), où le moindre changement réveille tout le monde, on préfère plusieurs petits contextes ciblés. Un changement de thème ne devrait pas re-render les composants qui ne lisent que l'utilisateur. Un contexte = une préoccupation qui évolue à son propre rythme. C'est plus de fichiers, mais beaucoup moins de re-renders parasites.

⚠️ Le Contexte n'est PAS un gestionnaire d'état global

Erreur fréquente : croire que useContext sert à « stocker l'état global de l'app ». Non. Le Contexte est un mécanisme de transport — il fait descendre une valeur dans l'arbre, point. Il ne gère pas le stockage, ne sélectionne pas finement qui se met à jour (il réveille tout le monde), et n'a pas de logique de mutation. Pour du véritable état global partagé (la session d'entraînement en cours, par exemple), Halterofit utilise un store dédié — c'est tout le sujet du module sur l'état global avec Zustand et MMKV. Règle pratique : Contexte pour transporter une valeur qui change rarement (thème, langue, utilisateur courant) ; store dédié pour l'état qui change souvent et que plein de composants lisent et modifient.

4. useMemo — se souvenir d'un calcul

💡 Le problème : recalculer pour rien

À chaque re-render, tout le corps de ta fonction re-tourne. Si tu as un calcul coûteux — filtrer une grosse liste, par exemple — il se referait intégralement à chaque rendu, même si ses ingrédients n'ont pas changé d'un poil. useMemo dit à React : « garde le résultat de ce calcul ; ne le refais que si l'un de ses ingrédients change vraiment ». Entre deux changements d'ingrédients, React te redonne paresseusement le résultat d'avant.

Voici l'usage réel dans ton hook d'entraînement. On veut afficher la liste des exercices visibles : ceux chargés depuis la base de données, filtrés pour ne garder que les ids encore présents dans la session en cours.

apps/mobile/src/hooks/workout/useActiveWorkout.ts
// On veut la liste des exercices VISIBLES : ceux de la base, filtrés
// par les ids encore présents dans la session.
const exercises = useMemo(() => {
  const dbExercises = workout?.exercises ?? [];
  if (dbExercises.length === 0) return dbExercises;

  // Construire ce Set + filtrer le tableau = du vrai travail. On ne veut
  // PAS le refaire à chaque rendu : pendant un entraînement, le chrono
  // déclenche un rendu chaque seconde (voir useElapsedSeconds).
  const visibleIds = new Set(exerciseIdsKey ? exerciseIdsKey.split(',') : []);
  return dbExercises.filter((e) => visibleIds.has(e.id));
}, [workout, exerciseIdsKey]); // ◀── les ingrédients (le tableau de dépendances)

La structure est useMemo(() => { calcul }, [ingrédients]) — exactement la même forme que useEffect du module sur useState & useEffect, mais ça retourne une valeur au lieu de produire un effet de bord. Tant que workout et exerciseIdsKey gardent la même référence, React redonne le même tableau exercises sans refiltrer.

🏋️ Dans Halterofit : pourquoi c'est justifié ici

Cet exemple est un cas d'école du « bon » useMemo, pas d'un memo décoratif. Pendant un entraînement, useElapsedSeconds fait avancer le chrono, ce qui provoque un re-render chaque seconde. Sans useMemo, on reconstruirait un Set et on refiltrerait toute la liste d'exercices 60 fois par minute, pour un résultat identique à chaque fois. Avec, le filtrage ne se relance que si workout (les données chargées) ou exerciseIdsKey (la signature des ids de la session) change réellement — c'est-à-dire quand on ajoute ou retire un exercice. Fréquence élevée × travail réel = pile la case où mémoïser vaut le coup.

🧭 Un détail malin : la clé en chaîne

Remarque que la dépendance n'est pas le tableau exerciseIds brut, mais exerciseIdsKey, c'est-à-dire exerciseIds.join(',') — une chaîne. Pourquoi ? Encore l'égalité référentielle. Un tableau change de référence à chaque écriture dans le store, ce qui relancerait le calcul (et le re-fetch) sans cesse. En le transformant en chaîne, on obtient une primitive qui ne change que si le contenu change vraiment — et les primitives, elles, se comparent par valeur. C'est une astuce très courante : convertir une donnée instable en clé stable.

5. useCallback — le même principe, mais pour une fonction

💡 Le problème : une fonction « neuve » à chaque rendu

Tu as déjà tout compris si tu as bien lu la section 1. Écrire () => {...} crée un nouvel objet-fonction à chaque exécution. Donc à chaque re-render, tes fonctions internes sont « neuves » au sens de ===, même si leur code est rigoureusement identique. Ça n'a aucune importance la plupart du temps... sauf quand tu passes cette fonction en prop à un composant enfant optimisé avec memo (section suivante) : il croit que la prop a changé et re-render inutilement. useCallback garde la même référence de fonction d'un rendu à l'autre, tant que ses dépendances ne changent pas.

// SANS useCallback : `onLog` est une fonction DIFFÉRENTE (nouvelle référence)
// à chaque rendu. Pour === elle n'est jamais "la même" qu'au rendu d'avant.
const onLog = () => logSet(exerciseId);

// AVEC useCallback : React garde la MÊME fonction tant que `exerciseId` ne
// change pas. Exactement la même structure que useMemo : (fonction, [deps]).
const onLog = useCallback(() => logSet(exerciseId), [exerciseId]);

Aide-mémoire : useMemo mémorise une valeur, useCallback mémorise une fonction. Les deux ne sont qu'une seule idée. D'ailleurs useCallback(fn, deps) est littéralement un raccourci pour useMemo(() => fn, deps) : « mémoïse cette fonction comme si c'était une valeur ».

Insistons sur un point que les débutants ratent souvent : useCallback seul, sur une fonction qui n'est passée à personne d'optimisé, ne sert à rien. Il ne « rend pas la fonction plus rapide ». Sa seule utilité est de stabiliser une référence pour que quelqu'un en aval (un memo, un autre useMemo, un tableau de dépendances d'useEffect) puisse constater « ah, c'est la même qu'avant, je ne bouge pas ». Sans ce destinataire, tu paies le coût de la mémoïsation sans aucun bénéfice. C'est exactement la transition vers memo.

6. memo — empêcher un composant de re-render pour rien

💡 Le problème : les enfants suivent le parent

Rappel du module sur le modèle mental de React : quand un parent re-render, ses enfants re-render aussi par défaut, en cascade, même si leurs propres props n'ont pas changé. La plupart du temps c'est sans conséquence (re-render, c'est pas cher). Mais si un enfant est lourd ou se trouve répété en grande quantité, c'est du gaspillage. React.memo(Composant) crée une version qui compare ses props au rendu précédent : si elles sont identiques, il saute son re-render et réutilise le rendu d'avant.

// MonGros est emballé dans memo : il ne se redessine QUE si ses props
// changent réellement, même si son parent re-render pour une autre raison.
const MonGros = React.memo(function MonGros({ data }) {
  return /* ... un rendu coûteux ... */;
});

Et voici le piège que tu peux maintenant anticiper tout seul, grâce à la section 1 : « props identiques », pour memo, ça veut dire mêmes références. Si tu passes en prop un objet ou une fonction fabriqués dans le parent, ils sont neufs à chaque rendu du parent. memo compare, voit une référence différente, conclut « les props ont changé » et re-render quand même. Ton memo ne sert à rien.

function Parent() {
  // PIÈGE : cet objet et cette fonction sont NEUFS à chaque rendu du Parent.
  // MonGros est memo... mais il re-render quand même, car la prop "change".
  return <MonGros data={{ x: 1 }} onPress={() => go()} />;
}

function ParentCorrige() {
  // On STABILISE les références. Maintenant memo "voit" qu'elles sont les
  // mêmes qu'avant et saute vraiment le re-render.
  const data = useMemo(() => ({ x: 1 }), []);
  const onPress = useCallback(() => go(), []);
  return <MonGros data={data} onPress={onPress} />;
}

C'est le moment où les trois outils se rejoignent.

7. Le trio qui ne marche qu'ensemble

💡 Une équipe, pas trois outils isolés

Tu peux maintenant relire les trois sections précédentes comme une seule histoire. memo est le seul qui apporte un gain visible (sauter un re-render coûteux), mais il ne fonctionne que si toutes les props sont stables. Or les props qui sont des objets ou des fonctions sont instables par nature (section 1). Donc useMemo sert à stabiliser les objets/valeurs passés en prop, et useCallback à stabiliser les fonctions passées en prop. Leur unique raison d'être, dans ce scénario, c'est de rendre memo capable de faire son travail. Enlève le memo en bout de chaîne, et les deux autres ne servent plus à rien.

OutilCe qu'il mémoriseÀ quoi ça sert
useMemo une valeur calculée (objet, tableau, résultat) éviter un recalcul coûteux ; stabiliser un objet passé en prop ou en valeur de contexte
useCallback une fonction stabiliser une fonction passée en prop (ou en dépendance)
memo le rendu d'un composant sauter le re-render si toutes les props sont restées identiques

La bonne façon de raisonner, dans cet ordre : « Ce composant enfant est-il vraiment lourd / très répété ? » → si oui, memo. « Lui passé-je des objets ou des fonctions en props ? » → si oui, stabilise-les avec useMemo / useCallback pour que le memo ne soit pas saboté. Tu n'arrives jamais à useCallback « parce que c'est mieux » ; tu y arrives parce qu'un memo précis en a besoin.

🧭 useRef : la valeur vraiment stable entre rendus

Un complément utile à connaître. Là où useMemo et useCallback stabilisent tant que les dépendances tiennent, useRef te donne une « boîte » dont la référence est garantie identique pour toute la vie du composant, quoi qu'il arrive. Tu y ranges une valeur via ref.current, tu la modifies sans déclencher de re-render. C'est l'outil quand tu veux te souvenir de quelque chose entre les rendus sans que ce souvenir ne provoque lui-même de rendu : l'id d'un minuteur, la valeur précédente d'une prop, une référence vers un élément. Garde-le dans un coin de ta tête comme « la mémoire stable et silencieuse ».

8. Le React Compiler — pourquoi tu dois comprendre sans sur-optimiser

Maintenant que tu as transpiré sur useMemo, useCallback et memo, une nouvelle qui surprend : dans Halterofit, tu vas en écrire à la main beaucoup moins que tu ne le penses. La raison s'appelle le React Compiler, et il est activé dans le projet.

Le React Compiler est un outil de build (relié à tout l'arsenal décrit dans le module sur l'outillage du projet) qui analyse ton code à la compilation et insère automatiquement la mémoïsation là où elle est utile. Concrètement, il comprend tout seul que tel objet ne dépend que de telles variables, et il fait l'équivalent d'un useMemo / useCallback / memo à ta place, sans que tu écrives ces hooks. Tu écris du code React simple et lisible, et la machine s'occupe de la plomberie d'optimisation en coulisses.

💡 Alors pourquoi avoir lu tout ce module ?

Excellente question, et la réponse est le cœur de ce guide. Tu n'apprends pas ces concepts pour les taper partout — le Compiler s'en charge — mais pour LIRE et comprendre le code. Quand tu tomberas sur un useMemo écrit à la main (comme celui de useActiveWorkout), tu sauras pourquoi il est là. Quand un composant ne se met pas à jour, ou se met à jour trop, tu auras le bon modèle mental — références, re-renders, dépendances — pour diagnostiquer. Le Compiler automatise le geste ; il n'automatise pas la compréhension. Et c'est justement la compréhension qui te rend autonome.

Conséquence pratique très concrète pour toi : ne sur-optimise pas à la main. Avec le Compiler activé, semer des useMemo / useCallback « au cas où » est non seulement inutile, c'est parfois contre-productif — tu ajoutes du bruit que la machine aurait géré mieux. Écris du code simple par défaut. Garde les optimisations manuelles pour les rares endroits où tu as un besoin précis et explicite, comme le filtrage du chrono. C'est moins de code à écrire, et surtout moins de code à lire pour le prochain qui passe — toi, dans trois mois.

✍️ Exercice de lecture

Reviens au useMemo de useActiveWorkout et à son tableau de dépendances :

const exercises = useMemo(() => {
  const dbExercises = workout?.exercises ?? [];
  if (dbExercises.length === 0) return dbExercises;
  const visibleIds = new Set(exerciseIdsKey ? exerciseIdsKey.split(',') : []);
  return dbExercises.filter((e) => visibleIds.has(e.id));
}, [workout, exerciseIdsKey]);

Trois questions : (1) Que se passerait-il si on oubliait exerciseIdsKey dans le tableau de dépendances ? (2) En quoi est-ce le même mécanisme de dépendances que le useEffect du module sur useState & useEffect ? (3) Sachant que ce composant re-render chaque seconde à cause du chrono, que retourne useMemo entre deux secondes si rien d'autre n'a changé — et pourquoi est-ce important pour l'égalité référentielle de exercises ?

Voir le corrigé

(1) Bug d'« état périmé » (stale). React garderait l'ancien résultat tant que seul exerciseIdsKey change — donc si l'utilisateur ajoute ou retire un exercice (ce qui modifie exerciseIdsKey mais pas forcément workout), la liste affichée ne se mettrait pas à jour. Le tableau de dépendances doit lister tout ce que le calcul lit à l'intérieur.

(2) C'est rigoureusement la même idée : « refais ce travail seulement si une de ces valeurs a changé ». useEffect applique ça à un effet de bord, useMemo à un calcul qui retourne une valeur, useCallback à une fonction. Apprends le concept de dépendances une seule fois : il sert pour les trois.

(3) Entre deux secondes, ni workout ni exerciseIdsKey ne changent, donc useMemo retourne exactement le même tableau (même référence) qu'à la seconde précédente. C'est précieux : si exercises est ensuite passé en prop à un composant memo (la liste d'exercices, par exemple), ce dernier verra une référence inchangée et pourra sauter son re-render. Si on refabriquait le tableau à chaque seconde, même avec un contenu identique, la référence changerait (section 1) et casserait ce memo en aval. Le useMemo protège donc deux fois : il évite le refiltrage et il garde une référence stable.

🧠 Quiz éclair

1. Pourquoi [] === [] vaut-il false en JavaScript, et quel rapport avec React ?

Parce que les objets (dont les tableaux et les fonctions) se comparent par référence (leur identité), pas par contenu : ce sont deux tableaux créés séparément, donc deux adresses différentes. Rapport avec React : à chaque rendu, ta fonction-composant recrée ses objets/fonctions, qui sont donc « différents » au sens de === — c'est exactement ce qui « casse » memo et justifie l'existence de useMemo / useCallback.

2. À quel problème répond useContext, et que se passe-t-il quand la valeur du Provider change ?

Au prop drilling : éviter de faire transiter une donnée à travers des composants intermédiaires. Un parent dépose la valeur (.Provider), un descendant la lit (useContext). Quand la valeur change (au sens de la référence), tous les consommateurs de ce contexte re-render — d'où l'intérêt de découper les contextes et de stabiliser les objets-valeur avec useMemo.

3. Différence en une phrase entre useMemo et useCallback ?

useMemo mémorise une valeur calculée ; useCallback mémorise une fonction. Même structure (…, [dépendances]), et useCallback(fn, d)useMemo(() => fn, d).

4. Tu as mis memo sur un composant lourd et il re-render quand même. Cause probable ?

On lui passe en prop un objet ou une fonction recréés à chaque rendu du parent : leur référence change à chaque fois, donc memo conclut « props différentes » et re-render. Il faut stabiliser ces props avec useMemo / useCallback pour que memo serve enfin.

5. Halterofit active le React Compiler. Faut-il donc encore comprendre cette mémoïsation ? Et faut-il l'écrire à la main partout ?

Comprendre : oui, c'est indispensable pour LIRE le code, diagnostiquer un re-render qui dérape, et savoir pourquoi un useMemo manuel est là. L'écrire partout : non — le Compiler insère la mémoïsation automatiquement, donc on écrit du code simple et on garde les optimisations manuelles pour les rares cas explicites (ex. le filtrage du chrono dans useActiveWorkout).

À retenir

Tout part d'un fait JavaScript : les objets se comparent par référence, pas par contenu ({} !== {}). Comme React recrée objets et fonctions à chaque rendu, et compare par référence, on a parfois besoin de stabiliser : useContext = lire une valeur fournie par un parent (anti prop-drilling, mais pas un état global) ; useMemo = mémoriser un calcul ou un objet ; useCallback = mémoriser une fonction ; memo = sauter le re-render d'un composant aux props inchangées. Les trois derniers forment une équipe et ne servent que face à un coût réel. Et puisque le React Compiler automatise tout ça, ta vraie mission est de comprendre, pas de mémoïser à la main partout.

⚠️ Piège fréquent

Deux pièges symétriques. Sur-optimiser : emballer tout dans useMemo / useCallback / memo « au cas où ». React est rapide par défaut, ces outils ont leur propre coût et alourdissent la lecture, et avec le React Compiler activé c'est souvent du bruit inutile. Mémoïser avec des dépendances incomplètes : oublier une valeur dans le tableau de dépendances donne un résultat périmé (stale) — le bug le plus sournois, car tout « marche » sauf dans le cas où la dépendance oubliée change. Liste toujours tout ce que le calcul utilise, ou n'optimise pas du tout.

🔄 Transférable

Deux idées que tu remporteras bien au-delà de React. D'abord la distinction référence vs valeur : elle existe dans presque tous les langages (Java, Python, C#…) et explique des milliers de bugs « pourquoi mes deux variables sont liées ? » ou « pourquoi ma comparaison échoue ? ». Ensuite la mémoïsation : « garder un résultat pour ne pas le recalculer » est un patron universel (caches, programmation dynamique, requêtes réseau). Dans les deux cas, comprendre le pourquoi — le coût des recalculs, l'identité des objets — vaut infiniment mieux que retenir une syntaxe.