Gestion de l'état avec React Hooks - Sans Redux et Context API

Bonjour à tous! Je m'appelle Arthur, je travaille sur VKontakte en tant qu'équipe Web mobile, je suis engagé dans le projet VKUI - une bibliothèque de composants React, à l'aide de laquelle certaines de nos interfaces dans les applications mobiles sont écrites. La question de travailler avec un État mondial est toujours ouverte pour nous. Il existe plusieurs approches bien connues: Redux, MobX, Context API. J'ai récemment rencontré un article d'André Gardi State Management avec React Hooks - No Redux ou Context API , dans lequel l'auteur suggère d'utiliser React Hooks pour contrôler l'état de l'application.

Les crochets font rapidement irruption dans la vie des développeurs, offrant de nouvelles façons de résoudre ou de repenser différentes tâches et approches. Ils changent notre compréhension non seulement de la façon de décrire les composants, mais aussi de la façon de travailler avec les données. Lisez la traduction de l'article et le commentaire du traducteur sous le chat.

image

Les crochets React sont plus puissants que vous ne le pensez


Aujourd'hui, nous étudierons React Hooks et développerons un hook personnalisé pour gérer l'état global de l'application, qui sera plus simple que l'implémentation Redux et plus productif que l'API Context.

Les bases de React Hooks


Vous pouvez ignorer cette partie si vous connaissez déjà les crochets.

useState ()


Avant l'apparition des crochets, les composants fonctionnels n'avaient pas la possibilité de définir un état local. La situation a changé avec l'avènement de useState() .



Cet appel renvoie un tableau. Son premier élément est une variable qui donne accès à la valeur d'état. Le deuxième élément est une fonction qui met à jour l'état et redessine le composant pour refléter les modifications.

 import React, { useState } from 'react'; function Example() { const [state, setState] = useState({counter:0}); const add1ToCounter = () => { const newCounterValue = state.counter + 1; setState({ counter: newCounterValue}); } return ( <div> <p>You clicked {state.counter} times</p> <button onClick={add1ToCounter}> Click me </button> </div> ); } 

useEffect ()


Les composants de classe répondent aux effets secondaires à l'aide de méthodes de cycle de vie telles que componentDidMount() . Le useEffect() vous permet de faire de même dans les composants fonctionnels.

Par défaut, les effets sont déclenchés après chaque redessin. Mais vous pouvez vous assurer qu'elles ne sont exécutées qu'après avoir modifié les valeurs de variables spécifiques, en leur passant le deuxième paramètre facultatif sous la forme d'un tableau.

 //     useEffect(() => { console.log('     '); }); //    useEffect(() => { console.log('     valueA'); }, [valueA]); 

Pour obtenir un résultat similaire à componentDidMount() , nous passerons un tableau vide au second paramètre. Étant donné que le contenu d'un tableau vide reste toujours inchangé, l'effet ne sera exécuté qu'une seule fois.

 //     useEffect(() => { console.log('    '); }, []); 

Partage d'état


Nous avons vu qu'un état de hook fonctionne exactement comme un état de composant de classe. Chaque instance de composant a son propre état interne.

Pour partager l'état entre les composants, nous allons créer notre propre crochet.



L'idée est de créer un tableau d'auditeurs et un seul état. Chaque fois qu'un composant change d'état, tous les composants abonnés appellent leur getState() et sont mis à jour pour cette raison.

Nous pouvons y parvenir en appelant useState() dans notre hook personnalisé. Mais au lieu de renvoyer la fonction setState() , nous l'ajoutons au tableau d'écouteurs et retournons une fonction qui met à jour en interne l'objet d'état et appelle tous les écouteurs.

Attends un instant. Comment cela me facilite-t-il la vie?


Oui, tu as raison. J'ai créé un package NPM qui encapsule toute la logique décrite.

Vous n'avez pas à l'implémenter dans chaque projet. Si vous ne voulez plus passer du temps à lire et que vous voulez voir le résultat final, ajoutez simplement ce package à votre application.

 npm install -s use-global-hook 

Pour comprendre comment travailler avec un package, étudiez des exemples dans la documentation. Et maintenant, je propose de me concentrer sur la façon dont le paquet est organisé à l'intérieur.

Première version


 import { useState, useEffect } from 'react'; let listeners = []; let state = { counter: 0 }; const setState = (newState) => { state = { ...state, ...newState }; listeners.forEach((listener) => { listener(state); }); }; const useCustom = () => { const newListener = useState()[1]; useEffect(() => { listeners.push(newListener); }, []); return [state, setState]; }; export default useCustom; 

Utilisation dans le composant


 import React from 'react'; import useCustom from './customHook'; const Counter = () => { const [globalState, setGlobalState] = useCustom(); const add1Global = () => { const newCounterValue = globalState.counter + 1; setGlobalState({ counter: newCounterValue }); }; return ( <div> <p> counter: {globalState.counter} </p> <button type="button" onClick={add1Global}> +1 to global </button> </div> ); }; export default Counter; 

Cette version fournit déjà l'état de partage. Vous pouvez ajouter un nombre arbitraire de compteurs à votre application, et ils auront tous un état global commun.

Mais nous pouvons faire mieux


Que voulez-vous:

  • supprimer l'écouteur du tableau lors du démontage du composant;
  • rendre le crochet plus abstrait à utiliser dans d'autres projets;
  • gérer initialState aide de paramètres;
  • réécrivez le crochet dans un style plus fonctionnel.

Appel d'une fonction juste avant de démonter un composant


Nous avons déjà découvert que l'appel de useEffect(function, []) avec un tableau vide fonctionne de la même manière que componentDidMount() . Mais si la fonction passée dans le premier paramètre renvoie une autre fonction, alors la deuxième fonction sera appelée juste avant de démonter le composant. Exactement comme componentWillUnmount() .

Ainsi, dans le code de la deuxième fonction, vous pouvez écrire la logique de suppression d'un composant d'un tableau d'écouteurs.

 const useCustom = () => { const newListener = useState()[1]; useEffect(() => { //     listeners.push(newListener); return () => { //     listeners = listeners.filter(listener => listener !== newListener); }; }, []); return [state, setState]; }; 

Deuxième version


En plus de cette mise à jour, nous prévoyons également:

  • passer le paramètre React et se débarrasser de l'importation;
  • exporter non customHook, mais une fonction qui renvoie customHook avec le initalState donné;
  • créer un objet de store qui contiendra la valeur d' state et la fonction setState() ;
  • remplacez les fonctions fléchées par celles habituelles dans setState() et useCustom() afin que vous puissiez associer le store à this .

 function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { //     this.listeners.push(newListener); return () => { //     this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.setState]; } const useGlobalHook = (React, initialState) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); return useCustom.bind(store, React); }; export default useGlobalHook; 

Séparer les actions des composants


Si vous avez déjà travaillé avec des bibliothèques de gestion d'état complexes, vous savez que la manipulation d'un état global à partir de composants n'est pas une bonne idée.

Il serait plus correct de séparer la logique métier en créant des actions pour changer l'état. Par conséquent, je souhaite que la dernière version du package fournisse un accès aux composants non pas à setState() , mais à un ensemble d'actions.

Pour ce faire, nous fournissons notre useGlobalHook(React, initialState, actions) troisième argument. Je veux juste ajouter quelques commentaires.

  • Les actions auront accès au store . De cette façon, les actions peuvent lire le contenu de store.state , mettre à jour l' store.setState() appelant store.setState() et même appeler d'autres store.actions en store.actions .
  • Pour éviter les dégâts, l'objet action peut contenir des sous-objets. Ainsi, vous pouvez transférer actions.addToCounter(amount ) à un sous-objet avec toutes les actions de compteur: actions.counter.add(amount) .

Version finale


L'extrait suivant est la version actuelle du package NPM use-global-hook .

 function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { this.listeners.push(newListener); return () => { this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.actions]; } function associateActions(store, actions) { const associatedActions = {}; Object.keys(actions).forEach((key) => { if (typeof actions[key] === 'function') { associatedActions[key] = actions[key].bind(null, store); } if (typeof actions[key] === 'object') { associatedActions[key] = associateActions(store, actions[key]); } }); return associatedActions; } const useGlobalHook = (React, initialState, actions) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); store.actions = associateActions(store, actions); return useCustom.bind(store, React); }; export default useGlobalHook; 

Exemples d'utilisation


Vous n'avez plus à gérer useGlobalHook.js . Vous pouvez maintenant vous concentrer sur votre application. Voici deux exemples d'utilisation du package.

Compteurs multiples, une valeur


Ajoutez autant de compteurs que vous le souhaitez: ils auront tous une valeur globale. Chaque fois qu'un des compteurs incrémentera l'état global, tous les autres seront redessinés. Dans ce cas, le composant parent n'a pas besoin d'être redessiné.
Exemple vivant .

Demandes ajax asynchrones


Recherchez les référentiels GitHub par nom d'utilisateur. Nous traitons les demandes ajax de manière asynchrone en utilisant async / wait. Nous mettons à jour le compteur de requêtes à chaque nouvelle recherche.
Exemple vivant .

Et bien c'est tout


Nous avons maintenant notre propre bibliothèque de gestion d'état sur React Hooks.

Commentaire du traducteur


La plupart des solutions existantes sont essentiellement des bibliothèques distinctes. En ce sens, l'approche décrite par l'auteur est intéressante en ce qu'elle n'utilise que les fonctionnalités intégrées de React. De plus, par rapport à la même API de contexte, qui sort également de la boîte, cette approche réduit le nombre de redessins inutiles et gagne donc en performances.

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


All Articles