Révolution ou douleur? Rapport Yandex React Hooks

Je m'appelle Artyom Berezin, je suis développeur de plusieurs services internes Yandex. Depuis six mois, je travaille activement avec React Hooks. Ce faisant, il y a eu des difficultés à surmonter. Maintenant, je veux partager cette expérience avec vous. Dans le rapport, j'ai examiné l'API React Hook d'un point de vue pratique - pourquoi avons-nous besoin de hooks, vaut-il la peine de changer, ce qui est préférable de prendre en compte lors du portage. Il est facile de faire des erreurs pendant la transition, mais les éviter n'est pas non plus si difficile.



- Les crochets ne sont qu'une autre façon de décrire la logique de vos composants. Il vous permet d'ajouter aux composants fonctionnels des fonctionnalités qui n'étaient auparavant inhérentes qu'aux composants des classes.



Tout d'abord, c'est le soutien de l'état interne, puis - le soutien des effets secondaires. Par exemple - demandes de réseau ou demandes à WebSocket: abonnement, désabonnement de certains canaux. Ou, peut-être, nous parlons de demandes à d'autres API de navigateur asynchrones ou synchrones. De plus, les crochets nous donnent accès au cycle de vie du composant, à son début de vie, c'est-à-dire au montage, à la mise à jour de ses accessoires et à sa mort.



Probablement la façon la plus simple d'illustrer en comparaison. Voici le code le plus simple qui ne peut être qu'avec un composant dans les classes. Le composant change quelque chose. Il s'agit d'un compteur régulier qui peut être augmenté ou diminué, un seul champ en état. En général, je pense que si vous êtes familier avec React, le code est complètement évident pour vous.



Un composant similaire qui remplit exactement la même fonction, mais écrit dans des crochets, semble beaucoup plus compact. Selon mes calculs, en moyenne, lors du portage de composants sur des classes vers des composants sur des crochets, le code diminue environ une fois et demie, et cela plaît.

Quelques mots sur le fonctionnement des crochets. Un hook est une fonction globale déclarée dans React et appelée à chaque fois qu'un composant est rendu. React suit les appels à ces fonctions et peut changer son comportement ou décider ce qu'il doit retourner.



Il existe certaines restrictions sur l'utilisation des crochets qui les distinguent des fonctions ordinaires. Tout d'abord, ils ne peuvent pas être utilisés dans des composants sur des classes, une telle restriction s'applique simplement car ils ne sont pas créés pour eux, mais pour des composants fonctionnels. Les crochets ne peuvent pas être appelés à l'intérieur de fonctions internes, à l'intérieur de boucles, de conditions. Uniquement au premier niveau d'imbrication, à l'intérieur des fonctions du composant. Cette restriction est imposée par React lui-même afin de pouvoir suivre les hooks appelés. Et il les empile dans un certain ordre dans son cerveau. Ensuite, si cet ordre change soudainement ou si certains disparaissent, des erreurs complexes, insaisissables, difficiles à déboguer sont possibles.

Mais si vous avez une logique assez compliquée et que vous souhaitez utiliser, par exemple, des crochets à l'intérieur des crochets, alors c'est très probablement un signe que vous devez faire un crochet. Supposons que vous établissiez plusieurs crochets connectés les uns aux autres dans un crochet personnalisé séparé. Et à l'intérieur, vous pouvez utiliser d'autres crochets personnalisés, créant ainsi une hiérarchie de crochets, mettant en évidence la logique générale.



Les crochets offrent certains avantages par rapport aux cours. Tout d'abord, comme suit du précédent, en utilisant des crochets personnalisés, vous pouvez tâtonner beaucoup plus facilement la logique. Auparavant, en utilisant l'approche avec des composants d'ordre supérieur, nous avons présenté une sorte de logique partagée, et c'était un wrapper sur le composant. Maintenant, nous mettons cette logique à l'intérieur des crochets. Ainsi, l'arborescence des composants est réduite: son imbrication est réduite et il devient plus facile pour React de suivre les modifications des composants, de recalculer l'arborescence, de recalculer le DOM virtuel, etc. Cela résout le problème de ce que l'on appelle l'enveloppeur-enfer. Ceux qui travaillent avec Redux, je pense, le savent.

Le code écrit à l'aide de crochets est beaucoup plus facile à minimiser avec des minimiseurs modernes comme Terser ou UglifyJS ancien. Le fait est que nous n'avons pas besoin de sauvegarder les noms des méthodes, nous n'avons pas besoin de penser aux prototypes. Après la transpilation, si la cible est ES3 ou ES5, nous obtenons généralement un tas de prototypes qui corrigent. Ici, tout cela n'a pas besoin d'être fait, il est donc plus facile de le minimiser. Et, comme nous n'utilisons pas de classes, nous n'avons pas besoin d'y penser. Pour les débutants, c'est souvent un gros problème et, probablement, l'une des principales raisons des bugs: nous oublions que cela peut être une fenêtre, que nous devons lier une méthode, par exemple, dans le constructeur ou d'une autre manière.

De plus, l'utilisation de crochets vous permet de mettre en évidence la logique qui contrôle tout effet secondaire. Auparavant, cette logique, en particulier lorsque nous avions plusieurs effets secondaires pour un composant, devait être divisée en différentes méthodes du cycle de vie du composant. Et, depuis l'apparition des crochets de minimisation, React.memo est apparu, maintenant les composants fonctionnels se prêtent à la mémorisation, c'est-à-dire que ce composant ne sera pas recréé ou mis à jour avec nous si ses accessoires n'ont pas changé. Cela ne pouvait pas être fait avant, maintenant c'est possible. Tous les composants fonctionnels peuvent être enveloppés dans un mémo. Également à l'intérieur du crochet useMemo est apparu, que nous pouvons utiliser pour calculer certaines valeurs lourdes, ou instancier certaines classes utilitaires une seule fois.

Le rapport sera incomplet si je ne parle pas de quelques crochets de base. Tout d'abord, ce sont des crochets de gestion d'état.



Tout d'abord - useState.



Un exemple est similaire à celui du début du rapport. useState est une fonction qui prend une valeur initiale et renvoie un tuple à partir de la valeur actuelle et de la fonction pour modifier cette valeur. Toute magie est servie par React en interne. Nous pouvons simplement lire cette valeur ou la modifier.

Contrairement aux classes, nous pouvons utiliser autant d'objets d'état que nécessaire, divise l'état en éléments logiques afin de ne pas les mélanger dans un seul objet, comme dans les classes. Et ces pièces seront complètement isolées les unes des autres: elles peuvent être changées indépendamment les unes des autres. Le résultat, par exemple, de ce code: on change deux variables, on calcule le résultat et on affiche les boutons qui nous permettent de changer la première variable ici et là, et la deuxième variable ici et là. Rappelez-vous cet exemple, car plus tard, nous ferons une chose similaire, mais beaucoup plus compliquée.



Il existe un tel useState sur les stéroïdes pour les amateurs de Redux. Il vous permet de changer l'état de manière plus cohérente à l'aide d'un réducteur. Je pense que ceux qui connaissent Redux ne peuvent même pas expliquer, pour ceux qui ne sont pas familiers, je vais le dire.

Un réducteur est une fonction qui accepte un état et un objet, généralement appelé action, qui décrit comment cet état doit changer. Plus précisément, il passe certains paramètres, et à l'intérieur du réducteur, il décide déjà, en fonction de leurs paramètres, de la façon dont l'état va changer, et par conséquent, un nouvel état doit être renvoyé, mis à jour.



De cette façon, il est utilisé dans le code du composant. Nous avons un crochet useReducer, il prend une fonction de réducteur, et le deuxième paramètre est la valeur initiale de l'état. Renvoie, comme useState, l'état actuel et la fonction pour le modifier est dispatch. Si vous transmettez un objet action à envoyer, nous invoquerons un changement d'état.



Utilisation très importante Crochet effet. Il vous permet d'ajouter des effets secondaires au composant, offrant une alternative au cycle de vie. Dans cet exemple, nous utilisons une méthode simple avec useEffect: il s'agit simplement de demander des données au serveur, avec l'API, par exemple, et d'afficher ces données sur la page.



UseEffect a un mode avancé, c'est lorsque la fonction passée à useEffect renvoie une autre fonction, alors cette fonction sera appelée dans le cycle suivant, lorsque cet useEffect sera appliqué.

J'ai oublié de mentionner, useEffect est appelé de manière asynchrone, juste après que le changement soit appliqué au DOM. Autrement dit, il garantit qu'il sera exécuté après le rendu du composant et peut conduire au rendu suivant si certaines valeurs changent.



Nous rencontrons ici pour la première fois un concept tel que les dépendances. Certains hooks - useEffect, useCallback, useMemo - prennent un tableau de valeurs comme deuxième argument, ce qui nous permettra de dire quoi suivre. Les changements dans ce tableau conduisent à une sorte d'effets. Par exemple, ici, hypothétiquement, nous avons une sorte de composant pour choisir un auteur dans une liste. Et une assiette avec des livres de cet auteur. Et lorsque l'auteur change, useEffect sera appelé. Lorsque cet authorId est modifié, une demande sera appelée et les livres seront chargés.

En outre, je mentionnerai en passant des crochets comme useRef, c'est une alternative à React.createRef, quelque chose de similaire à useState, mais les modifications apportées à ref ne conduisent pas au rendu. Parfois pratique pour certains hacks. useImperativeHandle nous permet de déclarer certaines «méthodes publiques» sur le composant. Si vous utilisez useRef dans le composant parent, il peut extraire ces méthodes. Pour être honnête, je l'ai essayé une fois à des fins éducatives, dans la pratique, ce n'était pas utile. useContext est juste une bonne chose, il vous permet de prendre la valeur actuelle du contexte si le fournisseur a défini cette valeur quelque part plus haut dans le niveau de la hiérarchie.

Il existe un moyen d'optimiser les applications React sur les hooks: la mémorisation. La mémorisation peut être divisée en interne et externe. D'abord à l'extérieur.



Il s'agit de React.memo, pratiquement une alternative à la classe React.PureComponent, qui suivait les changements d'accessoires et les composants modifiés uniquement lorsque les accessoires ou l'état changeaient.

Ici, une chose similaire, cependant, sans État. Il surveille également les modifications des accessoires et si les accessoires ont changé, un rendu se produit. Si les accessoires n'ont pas changé, le composant n'est pas mis à jour et nous économisons sur cela.



Méthodes internes d'optimisation. Tout d'abord, c'est une chose de bas niveau - useMemo, rarement utilisé. Il vous permet de calculer une valeur et de la recalculer uniquement si les valeurs spécifiées dans les dépendances ont changé.



Il existe un cas particulier de useMemo pour une fonction appelée useCallback. Il est principalement utilisé pour mémoriser la valeur des fonctions de gestionnaire d'événements qui seront transmises aux composants enfants afin que ces composants enfants ne puissent plus être rendus. Il est utilisé simplement. Nous décrivons une certaine fonction, l'enveloppons dans useCallback et indiquons de quelles variables elle dépend.

Beaucoup de gens ont une question, mais en avons-nous besoin? Avons-nous besoin de crochets? Déménageons-nous ou restons-nous comme avant? Il n'y a pas de réponse unique, tout dépend des préférences. Tout d'abord, si vous êtes directement lié de manière rigide à la programmation orientée objet, si vos composants, vous y êtes habitué en tant que classe, ils ont des méthodes qui peuvent être extraites, alors, probablement, cette chose peut vous sembler superflue. En principe, il m'a semblé, lorsque j'ai entendu parler des crochets pour la première fois, que c'était trop compliqué, une sorte de magie était ajoutée, et on ne savait pas pourquoi.

Pour les amateurs de fonctionnalités, c'est, disons, un incontournable, car les hooks sont des fonctions, et des techniques de programmation fonctionnelle leur sont applicables. Par exemple, vous pouvez les combiner ou faire n'importe quoi, en utilisant, par exemple, des bibliothèques telles que Ramda, etc.



Depuis que nous nous sommes débarrassés des classes, nous n'avons plus besoin de lier ce contexte aux méthodes. Si vous utilisez ces méthodes comme rappels. Habituellement, c'était un problème, car vous deviez vous rappeler de les lier dans le constructeur, ou d'utiliser une extension non officielle de la syntaxe du langage, comme des fonctions fléchées comme propriété. Pratique assez courante. J'ai utilisé mon décorateur, qui est aussi, en principe, expérimental, sur les méthodes.



Il y a une différence dans la façon dont le cycle de vie fonctionne, comment le gérer. Les crochets associent presque toutes les actions du cycle de vie au crochet useEffect, qui vous permet de vous abonner à la naissance et à la mise à jour d'un composant et à sa mort. Dans les classes, pour cela, nous avons dû redéfinir plusieurs méthodes, telles que componentDidMount, componentDidUpdate et componentWillUnmount. De plus, la méthode shouldComponentUpdate peut désormais être remplacée par React.memo.



Il y a une assez petite différence dans la façon dont l'État est géré. Tout d'abord, les classes ont un objet d'état. Nous avons dû entasser n'importe quoi là-bas. Dans les crochets, nous pouvons diviser l'état logique en quelques morceaux, qu'il serait pratique pour nous d'opérer séparément.

setState () des composants sur les classes autorisés à spécifier un patch d'état, modifiant ainsi un ou plusieurs champs de l'état. Dans les crochets, nous devons changer l'état entier dans son ensemble, et c'est même bien, car il est à la mode d'utiliser toutes sortes de choses immuables et de ne jamais s'attendre à ce que nos objets mutent. Ils sont toujours nouveaux avec nous.

La principale caractéristique des classes que les hooks n'ont pas: on pourrait s'abonner aux changements d'état. Autrement dit, nous changeons l'état et souscrivons immédiatement à ses modifications, en traitant impérativement quelque chose immédiatement après l'application des modifications. Dans les crochets, cela ne fonctionne tout simplement pas. Cela doit être fait d'une manière très intéressante, je vous le dirai plus loin.

Et un peu sur la façon fonctionnelle de mettre à jour. Cela fonctionne à la fois là et là, lorsque les fonctions de changement d'état acceptent une autre fonction, que cet état ne doit pas changer, mais plutôt créer. Et si dans le cas du composant class, il peut nous renvoyer une sorte de patch, alors dans les hooks, nous devons renvoyer la toute nouvelle valeur.

En général, il est peu probable que vous obteniez une réponse, que vous déménagiez ou non. Mais je conseille au moins d'essayer, au moins pour le nouveau code, de le ressentir. Lorsque je viens de commencer à travailler avec des crochets, j'ai immédiatement identifié plusieurs crochets personnalisés qui me conviennent pour mon projet. Fondamentalement, j'ai essayé de remplacer certaines des fonctionnalités que j'avais mises en œuvre via des composants d'ordre supérieur.



useDismounted - pour ceux qui connaissent RxJS, il est possible de se désabonner massivement de tous les observables dans un seul composant ou dans une seule fonction, en abonnant chaque observable à un objet spécial, sujet, et lorsqu'il est fermé, tous les abonnements sont annulés. C'est très pratique si le composant est complexe, s'il y a beaucoup d'opérations asynchrones à l'intérieur de l'Observable, il est pratique de se désinscrire de tout à la fois, et non de chacun séparément.

useObservable renvoie une valeur d'Observable quand une nouvelle apparaît là. Un hook useBehaviourSubject similaire revient de BehaviourSubject. Sa différence avec Observable est qu'elle a initialement une signification.

Le crochet personnalisé pratique useDebounceValue nous permet d'organiser, par exemple, une requête pour la chaîne de recherche, de sorte que chaque fois que vous n'appuyez pas sur une touche, n'envoyez pas quelque chose au serveur, mais attendez que l'utilisateur ait fini de taper.

Deux crochets similaires. useWindowResize renvoie les valeurs réelles actuelles des tailles de fenêtre. Le crochet suivant pour la position de défilement est useWindowScroll. Je les utilise pour raconter certaines fenêtres contextuelles ou fenêtres modales, s'il y a des choses compliquées qui ne peuvent tout simplement pas être faites avec CSS.

Et un si petit crochet pour implémenter des touches de raccourci, dont le composant, lorsqu'il est présent sur la page, il est abonné à une touche de raccourci. À sa mort, une désinscription automatique se produit.

À quoi servent ces crochets personnalisés? Que nous pouvons entasser un désabonnement à l'intérieur du crochet, et nous n'avons pas à penser à désinscrire manuellement quelque part dans le composant où ce crochet est utilisé.

Il n'y a pas si longtemps, ils m'ont jeté un lien vers la bibliothèque React-Use, et il s'est avéré que la plupart de ces crochets personnalisés y étaient déjà implémentés. Et j'ai écrit un vélo. C'est parfois utile, mais à l'avenir, très probablement, je vais probablement les jeter et utiliser React-Use. Et je vous conseille également de voir si vous comptez utiliser des crochets.



En fait, l'objectif principal du rapport est de montrer comment écrire de manière incorrecte, quels problèmes peuvent être et comment les éviter. La toute première chose, probablement ce que quiconque étudie ces crochets et essaie d'écrire quelque chose, est d'utiliser incorrectement useEffect. Voici le code similaire à celui que 100% tout le monde a écrit s'il essayait des hooks. Cela est dû au fait que useEffect est initialement perçu mentalement, comme une alternative à componentDidMount. Mais, contrairement à componentDidMount, qui n'est appelé qu'une seule fois, useEffect est appelé sur chaque rendu. Et l'erreur ici est qu'elle change, disons, la variable de données, et en même temps la changer conduit à un rendu de composant, par conséquent, l'effet sera redemandé. Ainsi, nous obtenons une série sans fin de demandes AJAX au serveur, et le composant lui-même se met constamment à jour, met à jour, met à jour.



Le réparer est très simple. Vous devez ajouter ici un tableau vide de ces dépendances dont il dépend, et les modifications qui redémarreront l'effet. Si nous avons une liste vide de dépendances spécifiée ici, l'effet, en conséquence, ne sera pas redémarré. Ce n'est pas une sorte de hack, c'est une fonctionnalité de base de l'utilisation de useEffect.



Disons que nous l'avons corrigé. Maintenant un peu compliqué. Nous avons un composant qui rend quelque chose qui doit être retiré du serveur pour une sorte d'ID. Dans ce cas, en principe, tout fonctionne bien jusqu'à ce que nous modifiions entityId dans le parent, ce n'est peut-être pas pertinent pour votre composant.



Mais très probablement, s'il change ou s'il est nécessaire de le changer, et que vous avez un ancien composant sur votre page et qu'il s'avère qu'il ne se met pas à jour, il est préférable d'ajouter ici entityId, en tant que dépendance, provoquant la mise à jour, mettant à jour les données.



Un exemple plus complexe avec useCallback. Ici, à première vue, tout va bien. Nous avons une certaine page qui a une sorte de compte à rebours, ou, inversement, une minuterie qui ne fait que cocher. Et, par exemple, une liste d'hôtes, et en haut sont des filtres qui vous permettent de filtrer cette liste d'hôtes. Eh bien, la maintenance a été ajoutée ici juste pour illustrer une valeur changeant fréquemment qui se traduit par un moteur de rendu.

, , maintenance , , , onChange. onChange, . , HostFilters - , , dropdown, . , . , .



onChange useCallback. , .

, . , , . Facebook, React. , , , , '. , , confusing .



? — , - , , , , , . .

, , , , , , . , Garbage Collector , . , , , , . , , , reducer, , . , .

, , . - , , setValue - , , setState . - useEffect.

useEffect - , - , , , useEffect. useEffect , . , , Backbone, : , , , - . , , - , . - . , , , , - . , , , , , , . .

, , . , , . , . , . , , , dropdown . , . dropdown pop-up, useWindowScroll, useWindowResize , . , , — , .

, , . , , , , , . , , , , , .



, «», . , , TypeScript . . , reducer Redux , action. , action , action. , , , .

. , action. , , IncrementA 0, 1, 2, . . , , , , . action action, - . UnionType “Action”, , , action. .

— . , initialState, . , - . TypeScript. . , typeState , initialState.



reducer. State, Action, : switch action.type. TypeScript UnionType: case, - , type. action .

, : , , . .



? , . . , reducer. , action creator , , dispatch.



extension Dev Tools. . .

, , . , , . useDebugValue , - Dev Tool. useConstants, - , loaded, , , .



— . , . , . , , , . , , — - , — .

. Facebook ESLint, . , , . , dependencies . , , , .

, , , - , . , , , . . , - - .

— , , - . , , . , , - . , . . :

Source: https://habr.com/ru/post/fr464071/


All Articles