Comment éviter ce piège de performance de React Hooks

Les crochets de réaction promettent d'éviter les frais généraux des composants de la classe tout en offrant les mêmes avantages. Par exemple, ils nous permettent d'écrire des composants fonctionnels avec état sans avoir à nous soucier de stocker l'état sur l'instance de la classe.

Cependant, l'écriture de composants stateful avec Hooks nécessite des précautions. Il existe une différence subtile entre la manière dont l’état est initialisé dans le constructeur d’un composant de classe et celui qui est initialisé avec le hook useState. Les développeurs qui comprennent déjà les composants de classe et qui considèrent les crochets simplement comme des composants de classe sans les éléments de classe courent le risque d'écrire des composants dont les performances sont inférieures à celles des composants de classe.

Ici, je discute d’une fonctionnalité de useState qui n’est mentionnée que brièvement dans la FAQ officielle de Hooks. Si vous comprenez cette fonctionnalité en détail, vous pourrez tirer le meilleur parti des crochets React. En plus de lire cette note, je vous invite à jouer avec Stress Testing React Hooks, un outil de référence que j'ai écrit pour illustrer ces particularités des crochets.

Les options avant de faire réagir les crochets

Supposons que certains calculs coûteux doivent être effectués une seule fois lors de la configuration de votre composant et supposons que ce calcul dépend de certains accessoires. Un composant fonctionnel simple fait un très mauvais travail à ceci:

Cela fonctionne très mal, car le calcul coûteux est effectué sur chaque rendu.

Les composants de classe améliorent cela en nous permettant d'effectuer une opération donnée une seule fois, par exemple dans le constructeur:

En stockant le résultat du calcul sur l’instance, dans ce cas à l’intérieur de l’état local du composant, nous pouvons contourner le calcul onéreux de chaque rendu ultérieur. Vous pouvez voir la différence que cela fait en comparant le composant de classe et le composant fonctionnel avec mon outil de référence.

Mais les composants de classe ont leurs propres inconvénients, comme mentionné dans la documentation officielle de React Hooks. C'est pourquoi les crochets ont été introduits.

Une implémentation naïve avec useState

Le hook useState peut être utilisé pour déclarer une "variable d'état" et lui donner une valeur initiale. Cette valeur peut être modifiée et utilisée lors de rendus ultérieurs. Dans cet esprit, vous pouvez essayer naïvement de procéder comme suit pour améliorer les performances de votre composant fonctionnel:

Vous pensez peut-être qu'étant donné que nous traitons ici avec un état partagé entre plusieurs rendus, le calcul coûteux est effectué uniquement sur le premier rendu, comme avec les composants de classe. Tu aurais tort.

Pour voir pourquoi, rappelez-vous que NaiveHooksComponent est juste une fonction, une fonction appelée à chaque rendu. Cela signifie que useState est invoqué à chaque rendu. Le fonctionnement de useState est une histoire compliquée qui ne doit pas nous concerner. Ce qui est important, c'est avec quoi useState est invoqué: il est invoqué avec la valeur de retour de CostCalculation. Mais nous ne saurons quelle est la valeur de ce retour si nous invoquons un calcul coûteux. En conséquence, notre composant NaiveHooks est condamné à effectuer le calcul coûteux sur chaque rendu, tout comme notre composant Functional précédent qui n’utilisait pas State.

Jusqu'à présent, useState ne nous apporte aucun avantage en termes de performances, comme le prouve mon outil de référence. (Bien entendu, le tableau renvoyé par useState contient également une fonction nous permettant de mettre facilement à jour la variable d'état, ce que nous ne pourrions pas faire avec un simple composant fonctionnel.)

Trois façons de mémoriser des calculs coûteux

Heureusement, les crochets de réaction nous offrent trois options de traitement d’état tout aussi performantes que les composants de classe.

1. useMemo

La première option consiste à utiliser le hook useMemo:

En règle générale, useMemo n'effectuera à nouveau le calcul coûteux que si la valeur de arg change. Ceci n’est cependant qu’une règle générale, car les futures versions de React peuvent parfois recalculer la valeur mémoisée.

Les deux options suivantes sont plus fiables.

2. Fonctions de passage à utiliserEtat

La deuxième option est de passer une fonction à useState:

Cette fonction n'est appelée que lors du premier rendu. C’est super utile. (Vous devez cependant vous rappeler que si vous souhaitez stocker une fonction réelle dans l’état, vous devez l’envelopper dans une autre fonction. Sinon, vous enregistrez la valeur de retour de la fonction au lieu de la fonction elle-même.)

3. useRef

La troisième option consiste à utiliser le hook useRef:

Celui-ci est un peu bizarre, mais cela fonctionne et il est officiellement sanctionné. useRef renvoie un objet ref mutable dont la clé actuelle pointe sur l'argument avec lequel useRef est invoqué. Cet objet de référence persistera dans les rendus suivants. Donc, si nous réglons la paresse comme nous le faisons ci-dessus, le calcul coûteux est effectué une seule fois.

Comparaison

Comme vous pouvez le constater avec mon outil de référence, ces trois options sont tout aussi performantes que notre composant de classe initial. Cependant, le comportement de useMemo peut changer dans le futur. Donc, si vous voulez avoir la garantie que le calcul coûteux est effectué une seule fois, vous devez utiliser l'option 2, qui transmet une fonction à useState, ou l'option 3, qui utilise useRef.

Le choix entre ces deux options dépend de la nécessité de mettre à jour le résultat du calcul coûteux. Considérez la différence entre l'option 2 et l'option 3 comme analogue à la différence entre stocker quelque chose dans this.state ou le stocker directement ici.