Comment faire des recherches d'utilisateurs sur GitHub en utilisant React + RxJS 6 + Recompose

Une image pour attirer l'attention


Cet article est destiné aux personnes ayant une expérience avec React et RxJS. Je partage juste des modÚles que j'ai trouvés utiles pour créer une telle interface utilisateur.


Voici ce que nous faisons:



Sans classes, travailler avec un cycle de vie ou setState .


La préparation


Tout ce dont vous avez besoin est dans mon référentiel sur GitHub.


 git clone https://github.com/yazeedb/recompose-github-ui cd recompose-github-ui yarn install 

Dans la branche master , il y a un projet terminé. Basculez vers la branche de start si vous voulez aller pas à pas.


 git checkout start 

Et exécutez le projet.


 npm start 

L'application devrait commencer Ă  localhost:3000 et voici notre interface utilisateur initiale.



Lancez votre éditeur préféré et ouvrez le src/index.js .



Recomposer


Si vous n'ĂȘtes pas familier avec Recompose , c'est une merveilleuse bibliothĂšque qui vous permet de crĂ©er des composants React dans un style fonctionnel. Il contient un large Ă©ventail de fonctions. Voici mes prĂ©fĂ©rĂ©s .


C'est comme Lodash / Ramda, React uniquement.


Je suis également trÚs heureux qu'il soutienne le modÚle Observer. Citant la documentation :


Il s'avĂšre que la plupart de l'API React Component peut ĂȘtre exprimĂ©e en termes de modĂšle Observer.

Aujourd'hui, nous allons pratiquer ce concept!


Composant en ligne


Jusqu'à présent, nous avons l' App - le composant React le plus courant. En utilisant la fonction componentFromStream de la bibliothÚque Recompose, nous pouvons l'obtenir à travers un objet observable.


La fonction componentFromStream démarre le rendu sur chaque nouvelle valeur à partir de notre observable. S'il n'y a pas encore de valeurs, il null .


La configuration


Les flux dans Recomposer suivent le document de proposition observable ECMAScript . Il décrit comment les objets observables doivent fonctionner lorsqu'ils sont implémentés dans les navigateurs modernes.


En attendant, nous utiliserons des bibliothĂšques telles que RxJS, xstream, most, Flyd, etc.


Recompose ne sait pas quelle bibliothĂšque nous utilisons, elle fournit donc la fonction setObservableConfig . Avec lui, vous pouvez convertir tout ce dont nous avons besoin en un ES Observable.


Créez un nouveau fichier dans le dossier src et nommez-le observableConfig.js .


Pour connecter RxJS 6 à Recomposer, écrivez-y le code suivant:


 import { from } from 'rxjs'; import { setObservableConfig } from 'recompose'; setObservableConfig({ fromESObservable: from }); 

Importez ce fichier dans index.js :


 import './observableConfig'; 

C’est tout!


Recomposer + RxJS


Ajoutez importation componentFromStream Ă  index.js :


 import { componentFromStream } from 'recompose'; 

Commençons par remplacer le composant App :


 const App = componentFromStream(prop$ => { ... }); 

Notez que componentFromStream prend comme argument une fonction avec le paramÚtre prop$ , qui est une version observable des props . L'idée est d'utiliser la carte pour transformer des props réguliers en composants React.


Si vous avez utilisĂ© RxJS, vous devez ĂȘtre familier avec l'opĂ©rateur de carte .


La carte


Comme son nom l'indique, la carte transforme Observable(something) en Observable(somethingElse) . Dans notre cas - Observable(props) dans Observable(component) .


Importez l'opérateur de map :


 import { map } from 'rxjs/operators'; 

Complétez notre composant App :


 const App = componentFromStream(prop$ => { return prop$.pipe( map(() => ( <div> <input placeholder="GitHub username" /> </div> )) ) }); 

Avec RxJS 5, nous utilisons pipe au lieu d'une chaĂźne d'instructions.


Enregistrez le fichier et vérifiez le résultat. Rien n'a changé!



Ajouter un gestionnaire d'événements


Nous allons maintenant rendre notre champ de saisie un peu réactif.


Ajoutez l'importation createEventHandler :


 import { componentFromStream, createEventHandler } from 'recompose'; 

Nous allons l'utiliser comme ceci:


 const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); return prop$.pipe( map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 

L'objet créé par createEventHandler a deux champs intéressants: handler et stream .


Sous le capot, le handler est un émetteur d'événements qui transfÚre des valeurs en stream . Et stream à son tour, est un objet observable qui transmet des valeurs aux abonnés.


Nous lierons stream et prop$ pour obtenir la valeur actuelle du champ de saisie.


Dans notre cas, un bon choix serait d'utiliser la fonction combineLatest .


ProblĂšme d'oeufs et de poulet


Pour utiliser combineLatest , stream et prop$ doivent produire des valeurs. Mais stream ne publiera rien tant qu'une certaine valeur ne libérera pas prop$ et vice versa.


Vous pouvez résoudre ce problÚme en définissant stream valeur initiale.


Importez l'instruction startWith depuis RxJS:


 import { map, startWith } from 'rxjs/operators'; 

Créez une nouvelle variable pour obtenir la valeur du stream mis à jour:


 // App component const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value) startWith('') ); 

Nous savons que le stream générera des événements lorsque le champ de saisie change, alors traduisons-les immédiatement en texte.


Et comme la valeur par défaut du champ de saisie est une chaßne vide, initialisez la value$ object avec la value$ ''


Tricoter ensemble


Nous sommes maintenant prĂȘts Ă  connecter les deux flux. Importez combineLatest comme mĂ©thode de crĂ©ation d'objets observables, pas comme opĂ©rateur .


 import { combineLatest } from 'rxjs'; 

Vous pouvez également importer une instruction tap pour examiner les valeurs d'entrée.


 import { map, startWith, tap } from 'rxjs/operators'; 

Utilisez-le comme ceci:


 const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value), startWith('') ); return combineLatest(prop$, value$).pipe( tap(console.warn), // <---      map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 

Maintenant, si vous commencez Ă  taper quelque chose dans notre champ de saisie, les valeurs [props, value] apparaĂźtront dans la console.



Composant utilisateur


Ce composant sera chargé d'afficher l'utilisateur dont nous lui transmettrons le nom. Il recevra de la value du composant App et le traduira en une demande AJAX.


Jsx / css


Tout cela est basé sur le merveilleux projet GitHub Cards . La plupart du code, en particulier les styles, est copié ou adapté.


Créez le dossier src/User . Créez-y un fichier User.css et copiez-y ce code .


Et copiez ce code dans le fichier src/User/Component.js .


Ce composant remplit simplement le modÚle avec les données d'un appel à l'API GitHub.


Conteneur


Maintenant, ce composant est "stupide" et nous ne sommes pas sur la route, faisons un composant "intelligent".


Voici src/User/index.js


 import React from 'react'; import { componentFromStream } from 'recompose'; import { debounceTime, filter, map, pluck } from 'rxjs/operators'; import Component from './Component'; import './User.css'; const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(user => ( <h3>{user}</h3> )) ); return getUser$; }); export default User; 

Nous avons défini User comme componentFromStream , qui renvoie un objet prop$ observable prop$ qui convertit les propriétés entrantes en <h3> .


debounceTime


Notre User recevra de nouvelles valeurs Ă  chaque pression sur une touche du clavier, mais nous n'avons pas besoin de ce comportement.


Lorsque l'utilisateur commence à taper, debounceTime(1000) ignore tous les événements qui durent moins d'une seconde.


arracher


Nous nous attendons à ce que l'objet user soit transmis en tant que props.user . L'opérateur de prélÚvement extrait le champ spécifié de l'objet et renvoie sa valeur.


filtrer


Ici, nous nous assurons que l' user passé et qu'il ne s'agit pas d'une chaßne vide.


carte


Nous créons la <h3> partir de l' user .


Connecter


Revenez Ă  src/index.js et importez le composant User :


 import User from './User'; 

Nous transmettons la valeur value comme paramĂštre user :


  return combineLatest(prop$, value$).pipe( tap(console.warn), map(([props, value]) => ( <div> <input onChange={handler} placeholder="GitHub username" /> <User user={value} /> </div> )) ); 

Maintenant, notre valeur est affichée avec un retard d'une seconde.



Pas mal, maintenant nous devons obtenir des informations sur l'utilisateur.


Demande de données


GitHub fournit une API pour obtenir des informations utilisateur: https://api.github.com/users/${user} . On peut facilement écrire une fonction auxiliaire:


 const formatUrl = user => `https://api.github.com/users/${user}`; 

Et maintenant, nous pouvons ajouter la map(formatUrl) aprĂšs le filter :


 const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), // <--   map(user => ( <h3>{user}</h3> )) ); 

Et maintenant, au lieu du nom d'utilisateur, l'URL s'affiche à l'écran.


Nous devons faire une demande! switchMap et ajax viennent Ă  la switchMap .


switchMap


Cet opérateur est idéal pour basculer entre plusieurs observables.


Disons que l'utilisateur a tapé le nom, et nous ferons une demande dans switchMap .


Que se passe-t-il si l'utilisateur entre quelque chose avant l'arrivée de la réponse de l'API? Faut-il s'inquiéter des demandes précédentes?


Non.


L' switchMap annulera l'ancienne demande et basculera vers la nouvelle.


ajax


RxJS fournit sa propre implémentation ajax qui fonctionne trÚs bien avec switchMap !


Essayez


Nous importons les deux opérateurs. Mon code ressemble à ceci:


 import { ajax } from 'rxjs/ajax'; import { debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators'; 

Et utilisez-les comme ceci:


 const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), switchMap(url => ajax(url).pipe( pluck('response'), map(Component) ) ) ); return getUser$; }); 

L' switchMap bascule de notre champ de saisie vers une requĂȘte AJAX. Lorsqu'une rĂ©ponse arrive, elle la transmet Ă  notre composant stupide.


Et voici le résultat!



Gestion des erreurs


Essayez d'entrer un nom d'utilisateur inexistant.



Notre application est cassée.


catchError


Avec l'opérateur catchError nous pouvons afficher une réponse saine au lieu de catchError tranquillement.


Nous importons:


 import { catchError, debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators'; 

Et insérez-le à la fin de notre demande AJAX:


 switchMap(url => ajax(url).pipe( pluck('response'), map(Component), catchError(({ response }) => alert(response.message)) ) ) 


Déjà pas mal, mais bien sûr vous pouvez faire mieux.


Erreur de composant


Créez le src/Error/index.js avec le contenu:


 import React from 'react'; const Error = ({ response, status }) => ( <div className="error"> <h2>Oops!</h2> <b> {status}: {response.message} </b> <p>Please try searching again.</p> </div> ); export default Error; 

Il affichera joliment la response et l' status notre demande AJAX.


Nous l'importons dans User/index.js , et en mĂȘme temps l'opĂ©rateur de RxJS:


 import Error from '../Error'; import { of } from 'rxjs'; 

N'oubliez pas que la fonction passée à componentFromStream doit retourner observable. Nous pouvons y parvenir en utilisant l'opérateur of:



 ajax(url).pipe( pluck('response'), map(Component), catchError(error => of(<Error {...error} />)) ) 

Maintenant, notre interface utilisateur est beaucoup mieux:



Indicateur de chargement


Il est temps de prĂ©senter la gestion de l'État. Sinon, comment pouvez-vous implĂ©menter l'indicateur de chargement?


Et si place setState nous utiliserons BehaviorSubject ?


La recomposition de la documentation suggĂšre ce qui suit:


Au lieu de setState (), combinez plusieurs threads

Ok, vous avez besoin de deux nouvelles importations:


 import { BehaviorSubject, merge, of } from 'rxjs'; 

Le BehaviorSubject contiendra l'état du téléchargement et la merge l'associera au composant.


componentFromStream intérieur componentFromStream :


 const User = componentFromStream(prop$ => { const loading$ = new BehaviorSubject(false); const getUser$ = ... 

Un BehaviorSubject initialisé avec une valeur initiale, ou "état". Puisque nous ne faisons rien tant que l'utilisateur ne commence pas à saisir du texte, initialisez-le à false .


Nous allons changer l'état loading$ utilisant l'opérateur tap :


 import { catchError, debounceTime, filter, map, pluck, switchMap, tap // <--- } from 'rxjs/operators'; 

Nous allons l'utiliser comme ceci:


 const loading$ = new BehaviorSubject(false); const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), tap(() => loading$.next(true)), // <--- switchMap(url => ajax(url).pipe( pluck('response'), map(Component), tap(() => loading$.next(false)), // <--- catchError(error => of(<Error {...error} />)) ) ) ); 

Juste avant la switchMap et AJAX, nous transmettons true à la valeur loading$ et false aprÚs la réponse réussie.


Et maintenant, nous connectons simplement le loading$ et getUser$ .


 return merge(loading$, getUser$).pipe( map(result => (result === true ? <h3>Loading...</h3> : result)) ); 

Avant de regarder le travail, nous pouvons importer l'instruction delay afin que les transitions ne soient pas trop rapides.


 import { catchError, debounceTime, delay, filter, map, pluck, switchMap, tap } from 'rxjs/operators'; 

Ajouter un delay avant la map(Component) :


 ajax(url).pipe( pluck('response'), delay(1500), map(Component), tap(() => loading$.next(false)), catchError(error => of(<Error {...error} />)) ) 

Résultat?



Tous :)

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


All Articles