Utilisation de RxJS dans React Development pour gérer l'état de l'application

L'auteur du document, dont nous publions la traduction aujourd'hui, dit qu'il souhaite ici démontrer le processus de développement d'une application React simple qui utilise RxJS. Selon lui, il n'est pas un expert en RxJS, car il étudie cette bibliothèque et ne refuse pas l'aide de personnes bien informées. Son objectif est d'attirer l'attention du public sur des façons alternatives de créer des applications React, pour inciter le lecteur à faire des recherches indépendantes. Ce matériel ne peut pas être appelé une introduction à RxJS. L'une des nombreuses façons d'utiliser cette bibliothèque dans le développement de React sera présentée ici.

image

Comment tout a commencé


Récemment, mon client m'a inspiré à apprendre à utiliser RxJS pour gérer l'état des applications React. Lorsque j'ai audité le code de l'application pour ce client, il voulait connaître mon opinion sur la façon dont il a développé l'application, étant donné qu'auparavant, elle avait utilisé exclusivement l'état local de React. Le projet a atteint un point où il était déraisonnable de compter uniquement sur React. Tout d'abord, nous avons parlé d'utiliser Redux ou MobX comme un meilleur moyen de gérer l'état de votre application. Mon client a créé un prototype pour chacune de ces technologies. Mais il ne s'est pas arrêté à ces technologies, créant un prototype d'application React qui utilise RxJS. À partir de ce moment, notre conversation est devenue beaucoup plus intéressante.

L'application en question était une plateforme de trading de crypto-monnaies. Il avait de nombreux widgets dont les données étaient mises à jour en temps réel. Les développeurs de cette application, entre autres, ont dû résoudre les tâches difficiles suivantes:

  • Gérez plusieurs demandes de données (asynchrones).
  • Mise à jour en temps réel d'un grand nombre de widgets sur le panneau de contrôle.
  • La solution au problème des widgets et de la connectivité des données, car certains widgets avaient besoin de données non seulement de certaines sources spéciales, mais aussi d'autres widgets.

En conséquence, les principales difficultés rencontrées par les développeurs n'étaient pas liées à la bibliothèque React elle-même, et en plus, je pouvais les aider dans ce domaine. Le principal problème était de faire fonctionner correctement les mécanismes internes du système, ceux qui reliaient les données de crypto-monnaie et les éléments d'interface créés par les outils React. C'est dans ce domaine que les capacités de RxJS ont été utiles, et le prototype qu'ils m'ont montré était très prometteur.

Utilisation de RxJS dans React


Supposons que nous ayons une application qui, après avoir effectué certaines actions locales, envoie des demandes à une API tierce . Il vous permet de rechercher par article. Avant d'exécuter la demande, nous devons obtenir le texte utilisé pour former cette demande. En particulier, nous, à l'aide de ce texte, formons une URL pour accéder à l'API. Voici le code du composant React qui implémente cette fonctionnalité

import React from 'react'; const App = ({ query, onChangeQuery }) => (  <div>    <h1>React with RxJS</h1>    <input      type="text"      value={query}      onChange={event => onChangeQuery(event.target.value)}    />    <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p>  </div> ); export default App; 

Ce composant n'a pas de système de gestion d'état. L'état de la propriété de query n'est stocké nulle part, la fonction onChangeQuery ne met pas non plus l'état à jour. Dans l'approche conventionnelle, un tel composant est équipé d'un système de gestion d'état local. Cela ressemble à ceci:

 class App extends React.Component { constructor(props) {   super(props);   this.state = {     query: '',   }; } onChangeQuery = query => {   this.setState({ query }); }; render() {   return (     <div>       <h1>React with RxJS</h1>       <input         type="text"         value={this.state.query}         onChange={event =>           this.onChangeQuery(event.target.value)         }       />       <p>{`http://hn.algolia.com/api/v1/search?query=${         this.state.query       }`}</p>     </div>   ); } } export default App; 

Cependant, ce n'est pas l'approche dont nous allons parler ici. Au lieu de cela, nous voulons établir un système de gestion de l'état des applications à l'aide de RxJS. Voyons comment procéder à l'aide d'un composant d'ordre supérieur (HOC).

Si vous le souhaitez, vous pouvez implémenter une logique similaire dans votre composant d' App , mais vous, très probablement, à un moment donné de votre travail sur l'application, décidez de concevoir un tel composant sous la forme d'un HOC qui peut être réutilisé.

Composants de premier ordre React et RxJS


Nous découvrirons comment gérer l'état des applications React à l'aide de RxJS, en utilisant un composant d'ordre supérieur à cet effet. Au lieu de cela, on pourrait implémenter le modèle d' accessoires de rendu. Par conséquent, si vous ne souhaitez pas créer vous-même un composant d'ordre supérieur à cet effet, vous pouvez utiliser les composants observables d'ordre supérieur Recompose avec mapPropsStream() et componentFromStream() . Dans ce guide, cependant, nous ferons tout par nous-mêmes.

 import React from 'react'; const withObservableStream = (...) => Component => { return class extends React.Component {   componentDidMount() {}   componentWillUnmount() {}   render() {     return (       <Component {...this.props} {...this.state} />     );   } }; }; const App = ({ query, onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default withObservableStream(...)(App); 

Alors que le composant d'ordre supérieur RxJS n'effectue aucune action. Il transfère uniquement son propre état et ses propres propriétés au composant d'entrée, qui devrait être étendu avec son aide. Comme vous pouvez le voir, au final, un composant d'ordre supérieur sera impliqué dans la gestion de l'état de React. Cependant, cet état sera obtenu à partir du débit observé. Avant de commencer à implémenter HOC et à l'utiliser avec le composant App , nous devons installer RxJS:

 npm install rxjs --save 

Commençons maintenant à utiliser un composant d'ordre supérieur et implémentons sa logique:

 import React from 'react'; import { BehaviorSubject } from 'rxjs'; ... const App = ({ query, onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); const query$ = new BehaviorSubject({ query: 'react' }); export default withObservableStream( query$, {   onChangeQuery: value => query$.next({ query: value }), } )(App); 

Le composant App lui-même ne change pas. Nous venons de passer deux arguments à un composant d'ordre supérieur. Nous les décrivons:

  • Objet observé. L'argument de query est un objet observable qui a une valeur initiale, mais, en outre, renvoie, au fil du temps, de nouvelles valeurs (car il s'agit d'un BehaviorSubject ). Tout le monde peut souscrire à cet objet observable. Voici ce que dit la documentation RxJS sur les objets de type BehaviorSubject : «L'une des options pour Subject objets Subject est un objet BehaviorSubject qui utilise le concept de« valeur actuelle ». Il stocke la dernière valeur transmise à ses abonnés et lorsqu'un nouvel observateur s'y abonne, il reçoit immédiatement cette «valeur actuelle» du BehaviorSubject . Ces objets sont bien adaptés à la présentation de données, dont de nouvelles parties apparaissent au fil du temps. "
  • Le système d'émission de nouvelles valeurs pour l'objet observé (déclencheur). La fonction onChangeQuery() , transmise via le HOC au composant App , est une fonction régulière qui transmet la valeur suivante à l'objet observé. Cette fonction est transférée dans l'objet, car il peut être nécessaire de transférer vers un composant d'ordre supérieur plusieurs fonctions qui effectuent certaines actions avec les objets observés.

Après avoir créé l'objet observé et vous y être abonné, le flux de la demande devrait fonctionner. Cependant, jusqu'à présent, le composant d'ordre supérieur lui-même ressemble à une boîte noire. Nous le réalisons:

 const withObservableStream = (observable, triggers) => Component => { return class extends React.Component {   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; 

Un composant d'ordre supérieur reçoit un objet observable et un objet avec des déclencheurs (peut-être que cet objet contenant des fonctions peut être appelé un terme plus réussi du lexique RxJS), représenté dans la signature de la fonction.

Les déclencheurs sont uniquement transmis via le HOC au composant d'entrée. C'est pourquoi le composant App reçoit directement la fonction onChangeQuery() , qui fonctionne directement avec l'objet observé, en lui passant de nouvelles valeurs.

L'objet observé utilise la méthode du cycle de vie componentDidMount() pour signer et la méthode componentDidMount() pour se désinscrire. La désinscription est nécessaire pour éviter les fuites de mémoire . Dans l'abonnement de l'objet observé, la fonction envoie uniquement toutes les données entrantes du flux au magasin d'état React local à l'aide de la commande this.setState() .

Apportons une petite modification au composant App , qui éliminera le problème qui se produit si le composant d'ordre supérieur ne définit pas la valeur initiale de la propriété de query . Si cela n'est pas fait, alors, au début du travail, la propriété de query s'avérera undefined . Grâce à cette modification, cette propriété obtient la valeur par défaut.

 const App = ({ query = '', onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); 

Une autre façon de résoudre ce problème consiste à définir l'état initial de la query dans un composant d'ordre supérieur:

 const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component {   constructor(props) {     super(props);     this.state = {       ...initialState,     };   }   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; const App = ({ query, onChangeQuery }) => ( ... ); export default withObservableStream( query$, {   onChangeQuery: value => query$.next({ query: value }), }, {   query: '', } )(App); 

Si vous essayez cette application maintenant, la zone de saisie devrait fonctionner comme prévu. Le composant App reçoit du HOC, sous forme de propriétés, uniquement l'état de la query et la fonction onChangeQuery pour changer l'état.

La récupération et la modification de l'état se produisent via des objets observables RxJS, malgré le fait qu'un magasin d'état React interne est utilisé à l'intérieur du composant d'ordre supérieur. Je n'ai pas pu trouver de solution évidente au problème de la transmission en continu de données à partir d'un abonnement d'objets observés directement vers les propriétés du composant étendu ( App ). C'est pourquoi j'ai dû utiliser l'état local de React sous la forme d'une couche intermédiaire, ce qui, en plus, est pratique en termes de causes de re-rendu. Si vous connaissez une autre façon d'atteindre les mêmes objectifs - vous pouvez la partager dans les commentaires.

Combiner les objets observés dans React


Créons un deuxième flux de valeurs, qui, comme la propriété de query , peut être utilisé dans le composant App . Plus tard, nous utiliserons les deux valeurs, en travaillant avec elles en utilisant un autre objet observable.

 const SUBJECT = { POPULARITY: 'search', DATE: 'search_by_date', }; const App = ({ query = '', subject, onChangeQuery, onSelectSubject, }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <div>     {Object.values(SUBJECT).map(value => (       <button         key={value}         onClick={() => onSelectSubject(value)}         type="button"       >         {value}       </button>     ))}   </div>   <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> </div> ); 

Comme vous pouvez le voir, le paramètre subject peut être utilisé pour affiner la demande lors de la construction de l'URL utilisée pour accéder à l'API. À savoir, les documents peuvent être recherchés en fonction de leur popularité ou de la date de publication. Ensuite, créez un autre objet observable qui peut être utilisé pour modifier le paramètre subject . Cet observable peut être utilisé pour associer un composant App à un composant d'ordre supérieur. Sinon, les propriétés transmises au composant App ne fonctionneront pas.

 import React from 'react'; import { BehaviorSubject, combineLatest } from 'rxjs/index'; ... const query$ = new BehaviorSubject({ query: 'react' }); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({   subject,   query, })), {   onChangeQuery: value => query$.next({ query: value }),   onSelectSubject: subject => subject$.next(subject), }, )(App); 

Le déclencheur onSelectSubject() n'est pas nouveau. Grâce à un bouton, il peut être utilisé pour basculer entre deux états de subject . Mais l'objet observé transféré à un composant d'ordre supérieur est quelque chose de nouveau. Il utilise la fonction combineLatest() de RxJS pour combiner les dernières valeurs renvoyées par deux (ou plus) flux observés. Après s'être abonné à l'objet observé, si l'une des valeurs ( query ou subject ) est modifiée, l'abonné recevra les deux valeurs.

Un complément au mécanisme implémenté par la fonction combineLatest() est son dernier argument. Ici, vous pouvez définir l'ordre de retour des valeurs générées par les objets observés. Dans notre cas, nous avons besoin qu'ils soient représentés comme un objet. Cela permettra, comme précédemment, de les déstructurer dans un composant d'ordre supérieur et de les écrire dans l'état React local. Étant donné que nous avons déjà la structure nécessaire, nous pouvons omettre l'étape d'encapsuler l'objet de l'objet de query observé.

 ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({   subject,   query, })), {   onChangeQuery: value => query$.next(value),   onSelectSubject: subject => subject$.next(subject), }, )(App); 

L'objet source, { query: '', subject: 'search' } , ainsi que tous les autres objets renvoyés par le flux combiné d'objets observés, conviennent pour les déstructurer dans un composant d'ordre supérieur et pour écrire les valeurs correspondantes dans l'état React local. Après la mise à jour de l'état, comme précédemment, le rendu est effectué. Lorsque vous lancez l'application mise à jour, vous devriez pouvoir modifier les deux valeurs à l'aide du champ de saisie et du bouton. Les valeurs modifiées affectent l'URL utilisée pour accéder à l'API. Même si une seule de ces valeurs change, l'autre valeur conserve son dernier état, car la fonction combineLatest() combine toujours les valeurs les plus récentes issues des flux observés.

Axios et RxJS dans React


Maintenant dans notre système, l'URL pour accéder à l'API est construite sur la base de deux valeurs à partir d'un objet observable combiné, qui comprend deux autres objets observables. Dans cette section, nous utiliserons l'URL pour charger les données de l'API. Vous pourrez peut-être utiliser le système de chargement de données React , mais lorsque vous utilisez des objets observables RxJS, vous devez ajouter un autre flux observable à notre application.

Avant de nous mettre au travail sur le prochain objet observé, installez axios . Il s'agit de la bibliothèque que nous utiliserons pour charger les données des flux dans le programme.

 npm install axios --save 

Imaginez maintenant que nous avons un tableau d'articles que le composant App devrait sortir. Ici, en tant que valeur du paramètre par défaut correspondant, nous utilisons un tableau vide, en faisant la même chose que pour les autres paramètres.

 ... const App = ({ query = '', subject, stories = [], onChangeQuery, onSelectSubject, }) => ( <div>   ...   <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p>   <ul>     {stories.map(story => (       <li key={story.objectID}>         <a href={story.url || story.story_url}>           {story.title || story.story_title}         </a>       </li>     ))}   </ul> </div> ); 

Pour chaque article de la liste, une valeur de repli est utilisée car l'API à laquelle nous accédons n'est pas uniforme. Maintenant - le plus intéressant - l'implémentation d'un nouvel objet observable qui est chargé de charger les données dans une application React qui les visualisera.

 import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { flatMap, map } from 'rxjs/operators'; ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); const fetch$ = combineLatest(subject$, query$).pipe( flatMap(([subject, query]) =>   axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ... 

Un nouvel objet observable est, encore une fois, une combinaison d'observables de subject et de query , car, pour construire l'URL avec laquelle nous accéderons à l'API pour charger les données, nous avons besoin des deux valeurs. Dans la méthode pipe() de l'objet observé, nous pouvons utiliser les soi-disant «opérateurs RxJS» afin d'effectuer certaines actions avec les valeurs. Dans ce cas, nous mappons deux valeurs placées dans la requête, qu'axios utilise pour obtenir le résultat. Ici, nous utilisons l'opérateur flatMap() et non map() pour accéder au résultat de la promesse résolue avec succès, et non à la promesse retournée elle-même. Par conséquent, après s'être abonné à ce nouvel objet observable, chaque fois qu'un nouveau subject ou une nouvelle valeur de query est entré dans le système à partir d'autres objets observables, une nouvelle query est exécutée et le résultat est dans la fonction d'abonnement.

Maintenant, encore une fois, nous pouvons fournir un nouveau observable à un composant d'ordre supérieur. Nous avons le dernier argument de la fonction combineLatest() notre disposition, cela permet de le mapper directement à une propriété appelée stories . En fin de compte, cela représente la façon dont ces données sont déjà utilisées dans le composant App .

 export default withObservableStream( combineLatest(   subject$,   query$,   fetch$,   (subject, query, stories) => ({     subject,     query,     stories,   }), ), {   onChangeQuery: value => query$.next(value),   onSelectSubject: subject => subject$.next(subject), }, )(App); 

Il n'y a pas de déclencheur ici, puisque l'objet observé est indirectement activé par deux autres flux observés. Chaque fois que la valeur du champ de saisie ( query ) est modifiée ou que le bouton ( subject ) est cliqué, cela affecte l'objet de fetch observé, qui contient les dernières valeurs des deux flux.

Cependant, il n'est peut-être pas nécessaire que chaque fois que nous modifions la valeur dans le champ de saisie, cela affectera l'objet de fetch observé. De plus, nous ne voudrions pas que la fetch soit affectée si la valeur est une chaîne vide. C'est pourquoi nous pouvons étendre l'objet de query observable à l'aide de l'instruction debounce , ce qui élimine les changements de requête trop fréquents. A savoir, grâce à ce mécanisme, un nouvel événement n'est accepté qu'après un temps prédéterminé après l'événement précédent. De plus, nous utilisons ici l'opérateur de filter , qui filtre les événements de flux si la chaîne de query est vide.

 import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest, timer } from 'rxjs'; import { flatMap, map, debounce, filter } from 'rxjs/operators'; ... const queryForFetch$ = query$.pipe( debounce(() => timer(1000)), filter(query => query !== ''), ); const fetch$ = combineLatest(subject$, queryForFetch$).pipe( flatMap(([subject, query]) =>   axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ... 

L'instruction debounce fait le travail de saisie de données dans le champ. Cependant, lorsqu'un bouton clique sur la valeur du subject , la demande doit être exécutée immédiatement.

Maintenant, les valeurs initiales de la query et du subject , que nous voyons lorsque le composant App est affiché pour la première fois, ne sont pas les mêmes que celles obtenues à partir des valeurs initiales des objets observés:

 const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); 

subject écrit en subject , une chaîne vide est en query . Cela est dû au fait que ce sont ces valeurs que nous avons fournies comme paramètres par défaut pour restructurer la fonction du composant App dans la signature. La raison en est que nous devons attendre la demande initiale exécutée par l'objet d' fetch observable. Étant donné que je ne sais pas exactement comment obtenir immédiatement des valeurs à partir d'objets de query et de subject observables dans un composant d'ordre supérieur afin de les écrire dans un état local, j'ai décidé de reconstituer l'état initial du composant d'ordre supérieur.

 const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component {   constructor(props) {     super(props);     this.state = {       ...initialState,     };   }   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; 

Maintenant, l'état initial peut être fourni à un composant d'ordre supérieur comme troisième argument. À l'avenir, nous pourrons supprimer les paramètres par défaut du composant App .

 ... const App = ({ query, subject, stories, onChangeQuery, onSelectSubject, }) => ( ... ); export default withObservableStream( combineLatest(   subject$,   query$,   fetch$,   (subject, query, stories) => ({     subject,     query,     stories,   }), ), {   onSelectSubject: subject => subject$.next(subject),   onChangeQuery: value => query$.next(value), }, {   query: 'react',   subject: SUBJECT.POPULARITY,   stories: [], }, )(App); 

Ce qui m'inquiète en ce moment, c'est que l'état initial est également défini dans la déclaration des objets observés query$ et subject$ . Cette approche est sujette aux erreurs, car l'initialisation des objets observés et l'état initial de la composante d'ordre supérieur partagent les mêmes valeurs. J'aurais préféré que les valeurs initiales soient extraites des objets observés dans un composant d'ordre supérieur pour définir l'état initial. Un des lecteurs de ce document pourra peut-être partager des conseils dans les commentaires sur la façon de procéder.

Le code du projet logiciel que nous avons réalisé ici se trouve ici .

Résumé


L'objectif principal de cet article est de démontrer une approche alternative au développement d'applications React à l'aide de RxJS. Nous espérons qu'il vous a donné matière à réflexion. Parfois, Redux et MobX ne sont pas nécessaires, mais peut-être que dans de telles situations, RxJS sera exactement ce qui convient à un projet spécifique.

Chers lecteurs! Utilisez-vous RxJS lors du développement d'applications React?

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


All Articles