Explication de useMemo, useCallback et memo
📌
useMemo
et useCallback
sont des hooks qui vont permettre la mise en cache du contenu afin de ne pas le recalculer si lors d’un nouveau rendu, les valeurs sont restées les mêmes.
Avant-propos
Quand un rendu est-il provoqué dans un composant ?
Un composant est re render lorsqu’un de ses state ou de ses props est mis à jour. Lorsque ce composant “parent” est rendu, il provoque également le rendu de tous ses enfants, qu’ils aient des props ou non.
Exemple : 1 seconde après le montage du parent, le state est mis à jour. Cela va provoquer un nouveau rendu de App, et donc de Item, mais aussi OtherItem.
const App = () => {
const [count, setCount] = useState(0);
const localVar = { obj: 0 };
console.log('render app');
useEffect(() => {
setTimeout(() => {
setCount(count + 1);
}, 1000);
}, []);
return <Item/>
}
const Item = () => {
console.log('render item');
return <OtherItem/>
}
const OtherItem = () => {
console.log('render otherItem');
return <p>yay</p>
}
// render app
// render item
// render otherItem
// ------ timeout passé ------
// render app
// render item
// render otherItem
memo
Pour empêcher le nouveau rendu inutile de Item (et par conséquent OtherItem), on va entourer la fonction Item dans le memo de React.
En gros, memo, c’est “si le composant et ses props n’ont pas changé, tu gardes la mise en cache”.
Celui-ci va donc mettre en cache la fonction, afin de ne pas la rendre de 0 à nouveau si rien n’a changé (ce qui est le cas ici).
...
const Item = memo(() => {
console.log('render item');
return <OtherItem/>
});
...
// render app
// render item
// render otherItem
// ------ timeout passé ------
// render app
UseMemo
Et avec des props ?
Dans la situation précédente, il n’y avait pas de props passés aux composants enfants.
Si par exemple, Item avait pris la variable localVar
en props, l’utilisation simple de memo
n’aurait pas suffit, et on aurait à nouveau eu les log de render pour Item et OtherItem après le setTimeout
.
Pourquoi ?
Il y a un concept à comprendre, les références de variables :
const a = { obj: 1 }
const b = { obj: 1 }
a === b // false
const c = a;
c === a // true
Lorsque le rendu d’un composant est provoqué, chaque référence de variable locale est démontée puis une nouvelle est montée.
Si on reprend l’exemple d’avant, la référence de la variable localVar
qui existait avant le setTimeout
a été démontée, elle n’existe plus. Elle a été remplacée par une nouvelle référence créée après le setTimeout, lors du nouveau rendu.
Démonstration :
- Rendu initial
- localVar référence est créée dans App
- localVar référence est passé à Item
- SetTimeout → setState → nouveau rendu de App
- localVar référence est démontée de App
- localVar référence est créée dans App
- React compare la localVar référence passée à Item et la localVar référence de App
- localVar référence ≠ localVar référence → nouveau rendu de Item et OtherItem
Solution :
On utilise le hook useMemo
pour mettre en cache la référence de la variable localVar
. À savoir que comme un setState
, il faut lui retourner une valeur.
const App = () => {
...
const localVar = useMemo(() => ({ obj: 0 }), []);
...
return <Item props={localVar}/>
}
const Item = memo(({props}) => {...});
- Rendu initial
- localVar référence est créée dans App et mise en cache via le useMemo
- localVar référence est passé à Item
- SetTimeout → setState → nouveau rendu de App
- localVar référence ne change pas, on garde la mise en cache
- React compare la localVar référence passée à Item et la localVar référence de App
- localVar référence = localVar référence → pas de nouveau rendu de Item et OtherItem
⚠️
Il est important de mémoiser chaque variable passée en props, sinon tous les autres memo seront inutiles, et il y aura un nouveau rendu malgré tout ce boilerplate.
Quels types de variables sont réinitalisées ?
const variable = useMemo(() => ({ obj: 1 }), []); // non
const variable = useMemo(() => (['tableau']), []); // non
const variable = { obj: 1 }; // réinitialisée
const variable = ['tableau']; // réinitialisée
const variable = 12345; // non
const variable = 'string'; // non
UseCallback
À la même manière que les variables locales, les fonctions
(qui sont au final aussi des variables) sont aussi démontées à chaque rendu.
On va donc utiliser useCallback
, qui fait la même chose que useMemo
, mais pour les fonctions.
const App = () => {
...
const localVar = useMemo(() => ({ obj: 0 }), []);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
...
return <Item local={localVar} click={handleClick}/>
}
const Item = memo(({props}) => {...});
Inconvénients
Le problème avec l’utilisation des memo
, c’est que la mise en cache est coûteuse.
C’est pendant le rendu initial que sont réalisées les mises en cache, ce qui va donc le ralentir.
L’avantage de ces memo n’arrive que lors des nouveaux rendus, lorsque l’on va éviter de recréer certaines données.
Mémoiser des variables simples, ou des fonctions qui ne font pas grand-chose en termes de calcul est assez inutile. Cela peut faire gagner quelques ms, mais rien de visible à l’oeil nu.
Exemple où l’utilisation des hooks devient efficace
Un cas d’utilisation où le processeur est beaucoup sollicité ; un map
d’un array pour afficher notre rendu (+ de 100 items).
Surtout si dans ce map, on calcule des choses pour chaque élément à afficher.
const List = ({countries}) => {
const content = useMemo(() => {
const sortedCountries = orderBy(countries, 'name', sort);
return sortedCountries.map(country => <Item country={country} key={country.id} />)
}, [countries, sort]);
return content;
};