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.
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,useCallbacketmemoé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
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.
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.
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
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 :
// 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 :
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 :
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.
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
À 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.
// 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.
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.
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
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
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
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.
| Outil | Ce 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.
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.
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.
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.
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).
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.
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.
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.