Aller au contenu principal

Améliorer les performances de React

· 21 minutes de lecture
Damien Buchet

Au fur et à mesure qu'une app React grossit, des problèmes de performance peuvent survenir (UI peu réactive, blocage de la page,...). Nous allons voir ici comment fonctionne "en gros" React, et comment optimiser ces performances.

Tout d'abord, commencons par les débuts de React, et notamment :

A quelle(s) problématique(s) répond React ?

Pour faire du "réactif" ?

Il est tout à fait possible de faire du "réactif" (une partie de la page seulement se met à jour en fonction d'actions utilisateur). Par exemple le code suivant affiche un compteur qui s'incrémente au clic sur un bouton, avec un state interne, comme on aurait l'habitude de l'écrire en React

class MyButton {
constructor(id) {
this.dom = document.getElementById(id);
this.state = { value: 0 };
this.onClick = () => {
this.setState({
value: this.state.value + 1,
});
};
this.render();
}
render() {
this.dom.innerHTML = `My counter value is ${this.state.value} <button>click me to inc!</button>`;
this.dom.querySelector('button').addEventListener('click', this.onClick);
}
setState(state) {
this.state = {
...this.state,
...state,
};
this.render();
}
}

export default MyButton;

React gère évidemment bien mieux et plus facilement tout cet aspect "réactif", mais ce n'est pas le besoin premier auquel réponds React

Pour les performances !

Il faut comprendre ce qui se passe dans le navigateur d'un user. Le navigateur va charger la structure HTML, le JS, le CSS, et à partir de là exécuter une phase dite de rendering & painting qui va faire en sorte que l'affichage voulu par le développeur, corresponde à ce que verra l'utilisateur final en appliquant le CSS et le JS. Puis en executant le javascript, qui peut lui aussi agir avec l'UI

Le "rendering" & "painting"

Ces phases de rendering & painting sont extrèment lente (on parle évidemment de millisecondes, mais des millisecondes pour un ordinateur... c'est lent !) A chaque phases de rendering & painting donc, votre navigateur va être "bloqué" et utiliser les ressources de votre ordinateur pour afficher la page. Vous avez surement déjà perçu ces phases, notamment sur des sites qui affichent une grande quantité d'information. Le site semble "bloqué" pendant quelques secondes avant que vous ne récupériez la main. Par exemple https://materialdesignicons.com/ qui est totalement bloqué en arrivant dessus, et prend à chaque recherche plusieurs secondes pour afficher les résultats.

Ainsi, dans l'exemple précédent, tout l'html est re-rendu, donc on relance une phase de rendering & painting à chaque clic sur le bouton. Dans cet exemple, très basique, le temps est négligeable, mais peut très rapidement devenir problématique sur des grosses applications.

React à la rescousse !

Alors comment React permet d'optimiser ces phases ? Bien évidemment, le premier "render" ne peux s'affranchir de ces phases. Mais pour éviter au maximum les rendering & painting suivants, React se base sur un Virtual DOM

Le Virtual DOM de React

Après le permier render de votre application, React va créer un Virtual DOM, c'est à dire une représentation virtuelle du DOM de votre page. A chaque interaction utilisateur, React va générer un nouveau Virtual DOM. Puis en comparant ces 2 Virtual DOM, React est capable de ne modifier dans le DOM réel que les éléments ayant besoin de changer.

Au lieu de recharger toute une partie de la page (voire toute la page), React est donc capable de ne modifier précisement que ce qui a changé entre 2 interactions, réduisant les phases de rendering & painting de ces changements au strict minimum, et donc quasiment imperceptible.

info

Les comparaisons de Virtual DOM (qui sont des objects javascript en mémoire) sont bien plus rapides que manipuler le DOM directement.

Si on reprend l'exemple du code du début, ce code va re-générer à chaque render tout l'HTML, du div, du button, et réaffecter le onClick à chaque render, tandis que React ne mettrait à jour que le compteur.


Les limites du Virtual DOM

Le Virtual DOM va dans 90% des cas être suffisant pour avoir des performances entièrement satisfaisante. Pourtant, dans les grosses applications (plusieurs centaines / millier de composants) le Virtual DOM arrive à ses limites.

Prenons en exemple le code suivant, d'après vous, est-ce que le console.log va être executé à chaque clic sur le bouton du compteur ? (Et donc etre re-render ?)

import React, { Component } from 'react';

class ChildrenComponent extends Component {
render() {
console.log('Rendered!');
return <div>This is a child component</div>;
}
}

class App extends Component {
state = { value: 0 };

onClick = () => {
this.setState(({ value }) => ({ value: value + 1 }));
};

render() {
return (
<>
<button onClick={this.onClick}>Count is {this.state.value}</button>
<ChildrenComponent />
</>
);
}
}

export default App;

On serait tenté de dire "non", et pourtant... le composant ChildrenComponent est bien re-render à chaque clic sur le bouton du compteur.

Pourquoi ca ? Car à chaque render d'un Component, React va comparer le Virtual DOM de ce composant, ainsi que celui de tous ses enfants (pour vérifier s'il y a besoin de mettre à jour le DOM). Or ChildrenComponent est un enfant de App, et comme App est re-render, tous ses enfant le seront aussi. Si ChildrenComponent avait aussi des enfants, ils auraient également été re-rendu.

En bref

Chaque fois qu'un composant est re-render, React va comparer son Virtual DOM, et celui de ses children pour vérifier s'il a besoin d'effectuer un changement dans le DOM.

On touche ici la problématique du Virtual DOM, la comparaison systématique des Virtual DOM de composants, qui à priori n'ont pas lieu de devoir changer.

info

Bien que le Virtual DOM soit plus rapide que manipuler directement le DOM, le Virtual DOM reste un object JS en mémoire assez lourd, et des comparaisons sur plusieurs centaines, voire millier de composants peuvent prendre du temps. La phase d'exécution du JS devient donc le facteur bloquant de l'application


Optimisation des Class Components

Méthode shouldComponentUpdate

React a alors introduit une nouvelle méthode de classe shouldComponentUpdate qui permet de définir "manuellement" si le composant doit être re-render (en retournant true) ou non (en retournant false)

import React, { Component } from 'react';

class ChildrenComponent extends Component {
render() {
console.log('Child 1 Rendered!');
return <div>This is a child component</div>;
}
}

class ChildrenComponent2 extends Component {
render() {
console.log('Child 2 Rendered!');
return <div>This is another child component</div>;
}
shouldComponentUpdate() {
return false;
}
}

class App extends Component {
state = { value: 0 };

onClick = () => {
this.setState(({ value }) => ({ value: value + 1 }));
};

render() {
return (
<>
<button onClick={this.onClick}>Count is {this.state.value}</button>
<ChildrenComponent />
<ChildrenComponent2 />
</>
);
}
}

export default App;

On voit bien ici que seul ChildrenComponent est re-render, contrairement à ChildrenComponent2 qui implémente un shouldComponentUpdate qui retourne false

Cette méthode shouldComponentUpdate vient s'excuter dans le cycle de vie des composants, juste avant le render, et prend en paramètres nextProps et nextState. nextProps et nextState sont les états des props et du state de ce composant calculé en vue d'un prochain render. Cela défini l'état "futur" du Composant

Cela permet donc de comparer manuellement this.props et nextProps ainsi que this.state & nextState. Et en fonction de la comparaison, retourner true pour re-render le Composant, ou false pour l'empêcher.

info

La méthode shouldComponentUpdate n'est PAS exécutée au render initial

Exemple

import React, { Component } from 'react';

class ChildrenComponent extends Component {
render() {
console.log('Child Rendered!');
return <div>Your name is {this.props.name}</div>;
}
shouldComponentUpdate(nextProps) {
return nextProps.name !== this.props.name;
}
}

class App extends Component {
state = {
value: 0,
name: 'John',
};

onClick = () => {
this.setState(({ value }) => ({ value: value + 1 }));
};

onClickChangeName = () => {
this.setState(({ name }) => ({ name: name === 'Doe' ? 'John' : 'Doe' }));
};

render() {
return (
<>
<button onClick={this.onClick}>Count is {this.state.value}</button>
<button onClick={this.onClickChangeName}>
Change Name to {this.state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={this.state.name} />
</>
);
}
}

export default App;
danger

Meme si votre shouldComponentUpdate retourne false, il empechera uniquement la phase de render du composant, mais les props de votre composant auront bien changés !

import React, { Component } from 'react';

class ChildrenComponent extends Component {
render() {
console.log('Child Rendered!');
return (
<div>
Your name is {this.props.name}
<button onClick={() => console.log(this.props.name)}>
Click me to know my inner name
</button>
</div>
);
}
shouldComponentUpdate() {
return false;
}
}

class App extends Component {
state = {
value: 0,
name: 'John',
};

onClick = () => {
this.setState(({ value }) => ({ value: value + 1 }));
};

onClickChangeName = () => {
this.setState(({ name }) => ({ name: name === 'Doe' ? 'John' : 'Doe' }));
};

render() {
return (
<>
<button onClick={this.onClick}>Count is {this.state.value}</button>
<button onClick={this.onClickChangeName}>
Change Name to {this.state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={this.state.name} />
</>
);
}
}

export default App;

Que doit-on comparer ?

Tout changement dans les props ou le state. React ne propose malheureusement pas une fonction propre aux changement des props et une propre au changement du state. Car normalement un changement de state doit provoquer un re-render. Si vous avez un Composant propsless (sans props), ce n'est pas la peine de vous encombrer avec un shouldComponentUpdate. Mais à partir du moment où on a des props, on va également devoir comparer le state.

On va donc, sur chaque Component, comparer les valeurs de nextProps et this.props. Ainsi que nextState et this.state Et se rendre compte rapidement, que c'est extrêment chronophage. Par exemple si on a 5 props différentes sur un Component, et 2 valeurs dans le state, on va se retrouver à devoir écrire :

shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.prop1 !== this.props.prop1 ||
nextProps.prop2 !== this.props.prop2 ||
nextProps.prop3 !== this.props.prop3 ||
nextProps.prop4 !== this.props.prop4 ||
nextProps.prop5 !== this.props.prop5 ||
nextState.prop1 !== this.props.prop1 ||
nextState.prop2 !== this.props.prop2
)
}

On se tournera donc rapidement vers une fonction "générique" qui permet de tester automatiquement toutes les props et le state, par exemple :

shouldComponentUpdate(nextProps, nextState) {
return (
Object.entries(nextProps).some(([key, value]) => value !== this.props[key]) ||
Object.entries(nextState).some(([key, value]) => value !== this.state[key])
)
}

Mais cela implique d'ajouter ce bout de code sur tous nos Components... Heureusement, React nous offre une solution !

Classe PureComponent

La classe PureComponent est une classe qui implémente par défaut un shouldComponentUpdate en comparant nextProps avec this.props et nextState avec this.state

Il suffit donc que notre Component extends PureComponent au lieu de extends Component et le tour est joué !

import React, { Component, PureComponent } from 'react';

class ChildrenComponent extends PureComponent {
render() {
console.log('Child Rendered!');
return <div>Your name is {this.props.name}</div>;
}
}

class App extends Component {
state = {
value: 0,
name: 'John',
};

onClick = () => {
this.setState(({ value }) => ({ value: value + 1 }));
};

onClickChangeName = () => {
this.setState(({ name }) => ({ name: name === 'Doe' ? 'John' : 'Doe' }));
};

render() {
return (
<>
<button onClick={this.onClick}>Count is {this.state.value}</button>
<button onClick={this.onClickChangeName}>
Change Name to {this.state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={this.state.name} />
</>
);
}
}

export default App;
En Bref

Si vous utilisez les composants, un rechercher / remplacer dans tous vos fichiers pour remplacer extends Component par extends PureComponent et le tour est joué ! (Bon oui, il faut changer les imports aussi)


Optimisation des Function Components

Alors, parfait pour les Composants, mais pour les composants fonction sans Classe ? Comment ca marche vu que ce ne sont pas des classes, et qu'on n'a donc pas accès à la méthode shouldComponentUpdate ?

Reprenons notre exemple précédent, transformé en function component

import React, { useState } from 'react';

const ChildrenComponent = ({ name }) => {
console.log('Child Rendered!');
return <div>Your name is {name}</div>;
};

const App = () => {
const [state, setState] = useState({
value: 0,
name: 'John',
});

const onClick = () => {
setState(state => ({
...state,
value: state.value + 1,
}));
};

const onClickChangeName = () => {
setState(state => ({
...state,
name: state.name === 'Doe' ? 'John' : 'Doe',
}));
};

return (
<>
<button onClick={onClick}>Count is {state.value}</button>
<button onClick={onClickChangeName}>
Change Name to {state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={state.name} />
</>
);
};

export default App;

De nouveau, notre ChildComponent se re-render lors du clic sur le compteur. On perd donc l'optimisation acquise avec les Components.

Hello memo

React nous offre le wrapper memo qui permet d'avoir le même comportement qu'un shouldComponentUpdate pour les Class.

import React, { useState, memo } from 'react';

const ChildrenComponent = memo(({ name }) => {
console.log('Child Rendered!');
return <div>Your name is {name}</div>;
});

const App = () => {
const [state, setState] = useState({
value: 0,
name: 'John',
});

const onClick = () => {
setState(state => ({
...state,
value: state.value + 1,
}));
};

const onClickChangeName = () => {
setState(state => ({
...state,
name: state.name === 'Doe' ? 'John' : 'Doe',
}));
};

return (
<>
<button onClick={onClick}>Count is {state.value}</button>
<button onClick={onClickChangeName}>
Change Name to {state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={state.name} />
</>
);
};

export default App;

C'est quoi memo ?

Bien que le comportement semble être le même qu'avec un shouldComponentUpdate, la mécanique est cependant différente. Les Function Components ne sont ni plus ni moins que des fonctions, qui sont executées pendant les phases de render.

Le fait de wrapper un composant avec memo va mémoriser (comme une mise en cache) le résultat de l'exécution de la fonction, pour des props données. Si la fonction est appelée de nouveau avec les mêmes props, la fonction n'est pas ré-executée mais on va renvoyer directement le résultat de la précédente exécution de cette fonction.

Si une des props de la fonction change entre 2 appels, la fonction est ré-executée, et son retour est de nouveau mémorisé à la place de l'ancien.

info

memo est une fonction de mémorisation à 1 niveau, ce qui signifie que chaque fois qu'une prop change, la fonction est ré-exectuée, et son retour mis en mémoire. On ne garde toujours que le retour de la dernière exécution en mémoire.

Il existe des librairies qui permettent de faire de la mémorisation à n niveaux, tel que memoizee

Du coup tout est bon, je peux optimiser les re-render inutile aussi simplement ? Réponse courte : oui mais 😀

Il manque une composante importante pour bien comprendre le fonctionnement de ces fonctions. Que nous allons voir tout de suite !

Les pièges de JS !

Comment React fait les comparaisons ?

React va effectuer une comparaison stricte entre les anciennes props et les nouvelles. C'est à dire une comparaison futureValue !== currentValue.

Faisons quelques essais

console.log(true === true)                  // ?
console.log("John" === "John") // ?
console.log(145 === 145) // ?
console.log({} === {}) // ?
console.log([] === []) // ?
console.log(new Date() === new Date()) // ?
console.log((() => null) === (() => null)) // ?

On arrive sur des problématiques qui ne sont pas propres à React, mais à Javascript. Mais qui peuvent rendre toutes nos optimisations précédentes complètement inutiles.

En quoi c'est problématique ?

React fait donc des comparaison stricte entre les anciennes props et les nouvelles. Et si une des props est différente, on re-exécute la fonction. Reprenons notre exemple, en ajoutant un style à notre ChildComponent :

import React, { useState, memo } from 'react';

const ChildrenComponent = memo(({ name, style }) => {
console.log('Child Rendered!');
return <div style={style}>Your name is {name}</div>;
});

const App = () => {
const [state, setState] = useState({
value: 0,
name: 'John',
});

const onClick = () => {
setState(state => ({
...state,
value: state.value + 1,
}));
};

const onClickChangeName = () => {
setState(state => ({
...state,
name: state.name === 'Doe' ? 'John' : 'Doe',
}));
};

return (
<>
<button onClick={onClick}>Count is {state.value}</button>
<button onClick={onClickChangeName}>
Change Name to {state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent
name={state.name}
style={{ backgroundColor: 'red', color: 'white' }}
/>
</>
);
};

export default App;

Pourquoi ChildComponent est re-executé dans ce cas ? Car Javascript ne sait pas comparer des objets (dans le sens où un humain les comparerait)

attention

En Javascript, les Objects (donc les Array, les Functions, les Dates,...) sont stockées sous forme de référence. Lorsqu'on compare 2 objects entre eux, Javascript va comparer les références de ces 2 objets, et en aucun cas leurs valeurs.

const obj = { foo: 'bar' };     // Attribue une reférence "ref1"
const obj2 = { foo: 'bar' }; // Attribue une référence "ref2"

obj === obj2 // "false" car Javascript compare les références, et "ref1" !== "ref2"
obj === obj // "true" car les références sont bien les memes : "ref1"

Vous avez déjà certainement croisé ce comportement sans forcement y prêter attention, lorsque vous manipulez des Objects

const obj = { foo: "bar" };
const obj2 = obj;

obj2.bar = "foo";
console.log(obj2);
console.log(obj);

Vous voyez ici que obj possède également bar: "foo". Car obj2 = obj ne fait que affecter la référence de obj à obj2. Les deux Objects ont donc la même référence, et chaque modification sur l'une ou l'autre des variables modifie le "contenu" de la même référence, donc le même objet.

Prenons maintenant cet exemple

const obj = { foo: "bar" };
const obj2 = { ...obj };

obj2.bar = "foo";
console.log(obj2);
console.log(obj);

Dans ce cas, nous avons bien 2 object différent car obj2 est un nouvel objet (et donc une référence différente) dans lequel nous venons copier toutes les valeurs de obj

astuce

On peut illustrer ce concept de référence avec des "tiroirs". Admettons qu'il y ai une étagère avec des tiroirs, nommés A-B-C... par ligne, et 1-2-3-4... par colonne.

Dans le 1er cas, vous placez 2 billes dans le tiroir A1. Un de vos amis vous demande dans quel tiroir vous avez rangé vos affaires, vous lui répondez A1. Vous lui avez donné la référence du tiroir. Votre ami place ensuite une bille supplémentaire dans ce tiroir. Lorsque vous reviendrez ouvrir votre tiroir A1, vous aurez bien 3 billes dedans.

Dans le second cas, vous placez 2 billes en A1. Votre amis va ouvrir un nouveau tiroir en A2, en copiant tout ce qu'il y avait à l'intérieur de votre tiroir A1. Puis rajouter sa bille dedans. Votre tiroir en A1 contient bien toujours uniquement vos 2 billes, tandis que celui de votre amis contiendra 3 billes

Et donc dans notre exemple ?

<ChildrenComponent name={state.name} style={{backgroundColor: "red", color: "white"}} />

A chaque render de notre composant App, je vais recréer un nouvel objet et le faire passer en tant que prop style. Donc mon composant ChildrenComponent va toujours comparer l'actuel style avec le "futur", et ces 2 objets n'ayant pas la meme référence, ils ne sont donc pas égaux pour Javascript, et la fonction est ré-executée.

Donc bien que notre ChildComponent soit memo, l'interet ici de memo est complètement anihilé par cette prop style qui changera de toute façon à chaque render de App.

Pire même, cela va ralentir l'execution de notre app, car à chaque render de App, React va comparer les props de ChildComponent alors qu'ils n'ont aucune chance d'etre égaux. On rajoute donc une instruction JS pour rien.

Exemple avec des handlers

import React, { useState, memo } from 'react';

const ButtonInc = memo(({ value, onClick }) => {
console.log(`Render button ${value}`);
return <button onClick={() => onClick(value)}>Inc by {value}</button>;
});

const App = () => {
const [state, setState] = useState({
value: 0,
});

const onClick = inc => {
setState(state => ({
...state,
value: state.value + inc,
}));
};

return (
<>
Count value is {state.value} <br />
<ButtonInc value={1} onClick={onClick} />
<ButtonInc value={5} onClick={onClick} />
<ButtonInc value={-10} onClick={onClick} />
</>
);
};

export default App;

Alors, pourquoi dans ce cas TOUS mes ButtonInc se re-executent ? Car de la meme manière que Javascript compare des références d'Object, il compare également des références de fonction. Et la fonction onClick est redéfinie à chaque render, c'est donc une nouvelle référence à chaque fois.

Mais du coup, vu que ce cas arrive dans quasiment tous les composants, memo est inutile ? Heureusement non, on va maintenant voir les Hooks de mémorisation


Hooks de mémorisation

Objets "statiques" ?

Tout d'abord, le cas le plus "facile", comme on l'a vu avec le style qui est un objet qui ne change jamais. Il nous suffit de sortir cette constante hors de mon composant, pour que l'Objet ne soit pas redéfini à chaque exécution, et garde donc toujours la même référence

import React, { useState, memo } from 'react';

const STYLE = { backgroundColor: 'red', color: 'white' };

const App = () => {
const [state, setState] = useState({
value: 0,
name: 'John',
});

const onClick = () => {
setState(state => ({
...state,
value: state.value + 1,
}));
};

const onClickChangeName = () => {
setState(state => ({
...state,
name: state.name === 'Doe' ? 'John' : 'Doe',
}));
};

return (
<>
<button onClick={onClick}>Count is {state.value}</button>
<button onClick={onClickChangeName}>
Change Name to {state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={state.name} style={STYLE} />
</>
);
};

const ChildrenComponent = memo(({ name, style }) => {
console.log('Child Rendered!');
return <div style={style}>Your name is {name}</div>;
});

export default App;

Objets "dynamique"

Evidemment cette solution ne fonctionne pas si on doit générer dynamiquement une des valeurs de l'object. React nous offre un "hook de mémorisation" useMemo

useMemo

useMemo est un hook au fonctionnement à mi chemin entre memo et useEffect. useMemo est un hook qui prend 2 paramètres : une fonction et un tableau de dépendance

const App = () => {
const memoized = useMemo(() => {
...
return something // Ce retour sera mémorisé tant qu'aucun changement n'intervient dans le tableau de dépendance
},
[] // Tableau de dépendance
)
}

La fonction en 1er paramètre sera executée au 1er render, et son retour mis en mémoire. Aux prochains renders du composant, la fonction ne sera pas ré-executée, mais retournera directement la valeur en mémoire. Si une des valeurs dans son tableau de dépendance change, la fonction sera de nouveau executée, et son retour mis en mémoire.

Exemple

import React, { useState, useMemo, memo } from 'react';

const App = () => {
const [state, setState] = useState({
value: 0,
name: 'John',
});

const onClick = () => {
setState(state => ({
...state,
value: state.value + 1,
}));
};

const onClickChangeName = () => {
setState(state => ({
...state,
name: state.name === 'Doe' ? 'John' : 'Doe',
}));
};

const isRed = state.value < 5 || state.value > 10;
const style = useMemo(() => {
const color = '#fff';
const backgroundColor = isRed ? 'red' : 'green';
return { color, backgroundColor };
}, [isRed]);

return (
<>
<button onClick={onClick}>Count is {state.value}</button>
<button onClick={onClickChangeName}>
Change Name to {state.name === 'John' ? 'Doe' : 'John'}
</button>
<ChildrenComponent name={state.name} style={style} />
</>
);
};

const ChildrenComponent = memo(({ name, style }) => {
console.log('Child Rendered!');
return <div style={style}>Your name is {name}</div>;
});

export default App;

Ici dans le tableau de dépendance, nous avons la variable isRed qui est un Boolean, donc qui peut etre soit true ou false. Tant que le valeur de ce booléen ne change pas, la fonction mémorisée par useMemo n'est pas ré-executée, et renvoie donc la même référence de l'objet style. Quand ce booléen change, la fonction est re-executée et la référence de style change, ce qui provoque bien le render de ChildComponent

info

Vous pouvez ajouter autant de dépendances que vous désirez dans le tableau de dépendance de useMemo. Si une seule de ces valeurs change, la fonction sera re-executée

const memoized = useMemo(() => ..., [value1, value2, value3, ...])

Si votre tableau de dépendance est vide, la fonction ne sera jamais re-executée et renverra toujours la valeur mémorisée. Attention il faut bien metre un tableau de dépendance vide, si vous omettez le tableau de dépendance, la fonction sera re-executée à chaque render

ATTENTION

React effectue des comparaisons strictes sur les éléments du tableau de dépendance de useMemo ! Il est donc très fortement déconseillé de passer autre chose que des primitives (Number, String, Boolean) dans les tableaux de dépendance des hooks.

Cette règle est la même pour useEffect et useCallback (qu'on verra plus tard)

Et pour les fonctions ?

On a vu que useMemo permettait de retourner une valeur mémorisée. On pourrait donc utiliser ce hook pour retourner notre fonction de handle

import React, { useState, useMemo, memo } from 'react';

const App = () => {
const [state, setState] = useState({
value: 0,
});

const onClick = useMemo(() => {
return inc => {
setState(state => ({
...state,
value: state.value + inc,
}));
};
}, []);

return (
<>
Count value is {state.value} <br />
<ButtonInc value={1} onClick={onClick} />
<ButtonInc value={5} onClick={onClick} />
<ButtonInc value={-10} onClick={onClick} />
</>
);
};

const ButtonInc = memo(({ value, onClick }) => {
console.log(`Render button ${value}`);
return <button onClick={() => onClick(value)}>Inc by {value}</button>;
});

export default App;

React nous propose un moyen d'écrire plus rapidement ce hook, sans avoir à retourner une fonction, le hook de mémorisation useCallback

useCallback

De la même manière que useMemo, useCallback est un hook qui prend 2 paramètres, une fonction et un tableau de dépendance. Mais là où useMemo mémorise le retour de la fonction, useCallback mémorise la fonction elle-même.

Concrètement, la fonction que vous faites passer en 1er paramètre sera mémorisée tant que le tableau de dépendance ne change pas. Le code précédent peut donc s'écrire :

import React, { useState, useCallback, memo } from 'react';

const App = () => {
const [state, setState] = useState({
value: 0,
});

const onClick = useCallback(inc => {
setState(state => ({
...state,
value: state.value + inc,
}));
}, []);

return (
<>
Count value is {state.value} <br />
<ButtonInc value={1} onClick={onClick} />
<ButtonInc value={5} onClick={onClick} />
<ButtonInc value={-10} onClick={onClick} />
</>
);
};

const ButtonInc = memo(({ value, onClick }) => {
console.log(`Render button ${value}`);
return <button onClick={() => onClick(value)}>Inc by {value}</button>;
});

export default App;

En bref

astuce

Utilisez memo, useMemo et useCallback pour éviter les re-render inutiles

attention

Les objets statiques qui ne changent pas, doivent être déclarés hors des composants fonction

danger

Evitez au maximum les non primitives dans les props et tableaux de dépendance des hooks

Exercices

Tous les codes suivant présentent une erreur de conception. Essayez de les retrouver ! Cliquez ensuite sur l'onglet "Solution" de chaques sections pour avoir la solution !

Exercice 1

import React, { useState } from 'react';

const ChildComponent = ({ name }) => {
return <div>Hello there, I'm {name}!</div>;
};

const App = () => {
const [value, setValue] = useState(0);
const onClick = () => setValue(v => v + 1);

return (
<>
<ChildComponent name="John" />
<div>
And I'm the counter you've clicked {value} times!{' '}
<button onClick={onClick}>Inc me!</button>
</div>
</>
);
};

export default App;

Exercice 2

import React, { memo, useState } from 'react';

const ChildComponent = memo(({ name, style }) => {
return <div style={style}>Hello there, I'm {name}!</div>;
});

const App = () => {
const [value, setValue] = useState(0);
const onClick = () => setValue(v => v + 1);

const style = { backgroundColor: 'red', color: 'white' };

return (
<>
<ChildComponent name="John" style={style} />
<div>
And I'm the counter you've clicked {value} times!{' '}
<button onClick={onClick}>Inc me!</button>
</div>
</>
);
};

export default App;

Exercice 3

import React, { memo, useState, useMemo } from 'react';

const ChildComponent = memo(({ name, style }) => {
return <div style={style}>Hello there, I'm {name}!</div>;
});

const App = () => {
const [value, setValue] = useState(0);
const onClick = () => setValue(v => v + 1);

const style = useMemo(() => {
return {
backgroundColor: ['red', 'black', 'cyan', 'white'][value % 4],
color: ['white', 'grey', 'red', 'black'][value % 4],
};
}, [value]);

return (
<>
<ChildComponent name="John" style={style} />
<div>
And I'm the counter you've clicked {value} times!{' '}
<button onClick={onClick}>Inc me!</button>
</div>
</>
);
};

export default App;

Exercice 4

import React, { memo, useState } from 'react';

const Button = memo(({ value, onClick }) => {
return <button onClick={() => onClick(value)}>{value}</button>;
});

const App = () => {
const [value, setValue] = useState(0);
const onClick = value => setValue(v => v + value);

return (
<>
<div>
Counter value is <b>{value}</b>
</div>
<div>
<Button value={1} onClick={onClick} />
<Button value={5} onClick={onClick} />
<Button value={-10} onClick={onClick} />
</div>
</>
);
};

export default App;

Exercice 5

import React, { memo, useState } from 'react';

const Button = memo(({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
});

const App = () => {
const [value, setValue] = useState(0);

return (
<>
<div>
Counter value is <b>{value}</b>
</div>
<div>
<Button onClick={() => setValue(1)} label="set as 1" />
<Button onClick={() => setValue(5)} label="set as 5" />
<Button onClick={() => setValue(-10)} label="set as -10" />
</div>
</>
);
};

export default App;

Exercice 6

import React, { memo, useState, useCallback } from 'react';

const Button = memo(({ onClick, value, label }) => {
return <button onClick={() => onClick(value)}>{label}</button>;
});

const App = () => {
const [value, setValue] = useState(0);
const onClick = useCallback(value => setValue(v => v + value));

return (
<>
<div>
Counter value is <b>{value}</b>
</div>
<div>
<Button onClick={onClick} value={1} label="Inc by 1" />
<Button onClick={onClick} value={5} label="Inc by 5" />
<Button onClick={onClick} value={-10} label="Dec by 10" />
</div>
</>
);
};

export default App;

Exercice 7

import React, { memo, useState, useCallback } from 'react';

const Button = memo(({ onClick, label }) => {
return <button onClick={onClick}>{label}</button>;
});

const App = () => {
const [value, setValue] = useState(0);
const onClick = useCallback(value => () => setValue(v => v + value), []);

return (
<>
<div>
Counter value is <b>{value}</b>
</div>
<div>
<Button onClick={onClick(1)} label="Inc by 1" />
<Button onClick={onClick(5)} label="Inc by 5" />
<Button onClick={onClick(-10)} label="Dec by 10" />
</div>
</>
);
};

export default App;