
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),
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),
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
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 :)