Comment construire la fonctionnalité de recherche GitHub dans React avec RxJS 6 et Recompose

Cet article est destiné aux personnes ayant une expérience de React et de RxJS. Je ne fais que partager des modèles que j’ai trouvés utiles lors de la création de cette interface utilisateur.

Voici ce que nous construisons:

Aucune classe, aucun cycle de cycle de vie ni setState.

Installer

Tout est sur mon GitHub.

le clone de git https://github.com/yazeedb/recompose-github-ui
cd recompose-github-ui
fil installer

La branche principale a le projet terminé. Vérifiez donc la branche de départ si vous souhaitez suivre.

git checkout start

Et lancez le projet.

npm start

L’application devrait fonctionner sur localhost: 3000, et voici notre interface utilisateur initiale.

Ouvrez le projet dans votre éditeur de texte favori et affichez src / index.js.

Recomposer

Si vous ne l’avez pas encore vu, Recompose est une magnifique ceinture utilitaire React pour la fabrication de composants dans un style de programmation fonctionnel. Il a une tonne de fonctions, et j’aurais du mal à choisir mes favoris.

C'est Lodash / Ramda, mais pour React.

J'aime aussi qu'ils supportent les observables. Citant les docs:

Il se trouve qu’une grande partie de l’API du composant React peut être exprimée en termes d’observables

Nous allons exercer ce concept aujourd'hui!

Streaming notre composant

À l'heure actuelle, App est un composant React ordinaire. Nous pouvons le renvoyer via un observable en utilisant la fonction ComponentFromStream de Recompose.

Cette fonction restitue initialement un composant nul et se redonne lorsque notre observable renvoie une nouvelle valeur.

Un trait de configuration

Les flux de recomposition suivent la proposition observable ECMAScript. Il explique comment les observables devraient fonctionner lorsqu'elles seront finalement envoyées aux navigateurs modernes.

Cependant, jusqu’à leur mise en œuvre complète, nous nous appuyons sur des bibliothèques telles que RxJS, xstream, la plupart, Flyd, etc.

Recompose ne sait pas quelle bibliothèque nous utilisons, il fournit donc un setObservableConfig pour convertir les observables ES en / de tout ce dont nous avons besoin.

Créez un nouveau fichier dans src appelé observableConfig.js.

Et ajoutez ce code pour rendre Recompose compatible avec RxJS 6:

importer {de} de 'rxjs';
import {setObservableConfig} de 'recomposer';
setObservableConfig ({
  deESObservable: de
});

Importez-le dans index.js:

importer './observableConfig';

Et nous sommes prêts!

Recomposer + RxJS

Importer le composantFromStream.

import Réagir de 'réagir';
importer ReactDOM de 'react-dom';
importer {composantFromStream} de 'recomposer';
importer './styles.css';
importer './observableConfig';

Et commencez à redéfinir App avec ce code:

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

Notez que composantFromStream accepte une fonction de rappel dans l'attente d'un flux prop $. L'idée est que nos accessoires deviennent observables et nous les mappons à un composant React.

Et si vous avez utilisé RxJS, vous connaissez l'opérateur idéal pour mapper les valeurs.

Carte

Comme son nom l'indique, vous transformez Observable (quelque chose) en Observable (quelque chose d'Else). Dans notre cas, observable (accessoires) en observable (composant).

Importer l'opérateur de la carte:

importer {map} de 'rxjs / operators';

Et redéfinir App:

const App = composantFromStream (prop $ => {
  retour prop $ .pipe (
    map (() => (
      
                    ))   ) });

Depuis RxJS 5, nous utilisons des canaux plutôt que des opérateurs en chaîne.

Enregistrez et vérifiez votre interface utilisateur, même résultat!

Ajout d'un gestionnaire d'événement

Nous allons maintenant rendre notre entrée un peu plus réactive.

Importez le createEventHandler de Recompose.

import {composantFromStream, createEventHandler} de 'recomposer';

Et utilisez-le comme suit:

const App = composantFromStream (prop $ => {
  const {handler, stream} = createEventHandler ();
  retour prop $ .pipe (
    map (() => (
      
                    ))   ) });

createEventHandler est un objet avec deux propriétés intéressantes: handler et stream.

Sous le capot, handler est un émetteur d’événements poussant des valeurs à diffuser, qui est un observable qui diffuse ces valeurs à ses abonnés.

Nous allons donc combiner le flux observable et le prop $ observable pour accéder à la valeur actuelle de l’entrée.

combineLatest est un bon choix ici.

Problème de poulet et d'oeufs

Pour utiliser combineLatest, cependant, stream et prop $ doivent émettre. stream n’émettra pas avant que prop $ n’émette, et inversement.

Nous pouvons résoudre ce problème en donnant à stream une valeur initiale.

Importer l'opérateur startWith de RxJS:

importer {map, startWith} depuis 'rxjs / operators';

Et créez une nouvelle variable pour capturer le flux modifié.

const {handler, stream} = createEventHandler ();
valeur const $ = stream.pipe (
  map (e => e.target.value),
  Commencer avec('')
)

Nous savons que le flux émettra des événements à partir de l’entrée onChange. Nous allons donc immédiatement mapper chaque événement sur sa valeur textuelle.

En plus de cela, nous initialiserons la valeur $ sous forme de chaîne vide - une valeur par défaut appropriée pour une entrée vide.

Tout combiner

Nous sommes prêts à combiner ces deux flux et à importer combineListest en tant que méthode de création, et non en tant qu’opérateur.

importer {combineLatest} de 'rxjs';

Vous pouvez également importer l'opérateur tap pour inspecter les valeurs telles qu'elles sont:

importer {map, startWith, tapez} à partir de 'rxjs / operators';

Et utilisez-le comme suit:

const App = composantFromStream (prop $ => {
  const {handler, stream} = createEventHandler ();
  valeur const $ = stream.pipe (
    map (e => e.target.value),
    Commencer avec('')
  )
  return combineLatest (prop $, valeur $). pipe (
    appuyez sur (console.warn),
    map (() => (
      
                    ))   ) });

Maintenant que vous tapez, [props, valeur] est enregistré.

Composant utilisateur

Ce composant sera chargé d'extraire / d'afficher le nom d'utilisateur que nous lui donnons. Il recevra la valeur d'App et le mappera sur un appel AJAX.

JSX / CSS

Tout est basé sur ce projet génial de cartes GitHub. La plupart des éléments, en particulier les styles, sont copiés / collés ou retravaillés pour s’adapter à React et aux accessoires.

Créez un dossier src / utilisateur et placez ce code dans User.css:

Et ce code dans src / User / Component.js:

Le composant remplit simplement un modèle avec la réponse JSON standard de l’API GitHub.

Le conteneur

Maintenant que le composant "bête" est à l’écart, passons au composant "intelligent":

Voici src / User / index.js:

import Réagir de 'réagir';
importer {composantFromStream} de 'recomposer';
importer {
  debounceTime,
  filtre,
  carte,
  cueillir
} de 'rxjs / operators';
importer un composant de './Component';
import './User.css';
const Utilisateur = composantFromStream (prop $ => {
  const getUser $ = prop $ .pipe (
    debounceTime (1000),
    cueillette ('utilisateur'),
    filtre (utilisateur => utilisateur && utilisateur.length),
    map (utilisateur => (
      

{utilisateur}     ))   )

  return getUser $;
});
export utilisateur par défaut;

Nous définissons l'utilisateur comme un composantFromStream, qui renvoie un prop $ stream mappé à un

.

debounceTime

Étant donné que User recevra ses accessoires via le clavier, nous ne souhaitons pas écouter chaque émission.

Lorsque l'utilisateur commence à taper, debounceTime (1000) ignore toutes les émissions pendant une seconde. Ce schéma est communément utilisé dans les types-aheads.

cueillir

Ce composant attend prop.user à un moment donné. plume attrape l’utilisateur, nous n’avons donc pas besoin de déstructurer nos accessoires à chaque fois.

filtre

Assure que cet utilisateur existe et n’est pas une chaîne vide.

carte

Pour l'instant, il suffit de placer l'utilisateur dans une balise

.

Accrocher

De retour dans src / index.js, importez le composant User:

importer l'utilisateur de './User';

Et fournissez de la valeur en tant que prop utilisateur:

  return combineLatest (prop $, valeur $). pipe (
    appuyez sur (console.warn),
    map (([props, value]) => (
      
        
        
      
    ))
  )

Votre valeur est maintenant affichée à l'écran après 1 seconde.

Bon début, mais nous devons aller chercher l'utilisateur.

Récupérer l'utilisateur

L’API utilisateur de GitHub est disponible à l’adresse https://api.github.com/users/${user}. Nous pouvons facilement extraire cela dans une fonction d'assistance à l'intérieur de User / index.js:

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

Maintenant, nous pouvons ajouter une carte (formatUrl) après le filtre:

Vous remarquerez que le noeud final de l'API est affiché à l'écran après 1 seconde maintenant:

Mais nous devons faire une demande d'API! Voici switchMap et ajax.

switchMap

Également utilisé dans les sauts de caractères, switchMap est idéal pour passer littéralement d’un observable à un autre.

Supposons que l’utilisateur entre un nom d’utilisateur et nous le récupérons dans switchMap.

Que se passe-t-il si l'utilisateur entre quelque chose de nouveau avant que le résultat ne revienne? Est-ce que nous nous soucions de la réponse précédente de l'API?

Nan.

switchMap annulera cette extraction précédente et se concentrera sur celle en cours.

ajax

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

Les utiliser

Importons les deux. Mon code ressemble à ceci:

importer {ajax} de 'rxjs / ajax';
importer {
  debounceTime,
  filtre,
  carte,
  cueillir,
  switchMap
} de 'rxjs / operators';

Et utilisez-les comme suit:

const Utilisateur = composantFromStream (prop $ => {
  const getUser $ = prop $ .pipe (
    debounceTime (1000),
    cueillette ('utilisateur'),
    filtre (utilisateur => utilisateur && utilisateur.length),
    carte (formatUrl),
    switchMap (url =>
      ajax (url) .pipe (
        plumer ('réponse'),
        carte (composant)
      )
    )
  )
  return getUser $;
});

Basculez de notre flux d'entrée vers un flux de requêtes ajax. Une fois la demande terminée, récupérez sa réponse et associez-la à notre composant User.

Nous avons un résultat!

La gestion des erreurs

Essayez d’entrer un nom d’utilisateur qui n’existe pas.

Même si vous le changez, notre application est défectueuse. Vous devez actualiser pour aller chercher plus d'utilisateurs.

C’est une mauvaise expérience utilisateur, non?

catchError

Avec l'opérateur catchError, nous pouvons restituer une réponse raisonnable à l'écran au lieu de le casser en silence.

Importez-le:

importer {
  catchError,
  debounceTime,
  filtre,
  carte,
  cueillir,
  switchMap
} de 'rxjs / operators';

Et collez-le à la fin de votre chaîne ajax.

switchMap (url =>
  ajax (url) .pipe (
    plumer ('réponse'),
    carte (composant),
    catchError (({response}) => alert (response.message))
  )
)

Au moins, nous avons des retours, mais nous pouvons faire mieux.

Un composant d'erreur

Créez un nouveau composant, src / Error / index.js.

import Réagir de 'réagir';

const Erreur = ({réponse, statut}) => (
  
    

Oups!            {status}: {response.message}          

Veuillez réessayer.    ) erreur d'exportation par défaut;

Cela affichera bien la réponse et le statut de notre appel AJAX.

Importons-le dans User / index.js:

import Error from '../Error';

Et de RxJS:

importer {of} de 'rxjs';

Rappelez-vous que notre rappel de composantFromStream doit renvoyer un observable. Nous pouvons y parvenir avec.

Voici le nouveau code:

ajax (url) .pipe (
  plumer ('réponse'),
  carte (composant),
  catchError (error => of ())
)

Il suffit de répandre l'objet d'erreur comme accessoire sur notre composant.

Maintenant, si nous vérifions notre interface utilisateur:

Beaucoup mieux!

Un indicateur de chargement

Normalement, nous avons maintenant besoin d’une forme de gestion d’état. Sinon, comment construire un indicateur de chargement?

Mais avant d’atteindre setState, voyons si RxJS peut nous aider.

Les documents de recomposition m'ont amené à penser dans cette direction:

Au lieu de setState (), combinez plusieurs flux.

Edit: Au départ, j'avais utilisé BehaviorSubjects, mais Matti Lankinen a répondu avec un moyen génial de simplifier ce code. Merci Matti!

Importer l'opérateur de fusion.

importer {merge, of} de 'rxjs';

Lorsque la demande est faite, nous allons fusionner notre ajax avec un flux de chargement de composant.

À l'intérieur du composantFromStream:

const Utilisateur = composantFromStream (prop $ => {
  const loading $ = of (

Chargement en cours ... );   const getUser $ = ...

Un simple indicateur de charge h3 transformé en observable! Et utilisez-le comme suit:

const loading $ = of (

Chargement en cours ... );

const getUser $ = prop $ .pipe (
  debounceTime (1000),
  cueillette ('utilisateur'),
  filtre (utilisateur => utilisateur && utilisateur.length),
  carte (formatUrl),
  switchMap (url =>
    fusionner(
      chargement $,
      ajax (url) .pipe (
        plumer ('réponse'),
        carte (composant),
        catchError (error => of ())
      )
    )
  )
)

J'aime à quel point c'est concis. En entrant dans switchMap, fusionnez les observables de chargement $ et ajax.

Comme le chargement de $ est une valeur statique, il émettra d’abord. Une fois que l'ajax asynchrone se termine, il sera émis et affiché à l'écran.

Avant de le tester, nous pouvons importer l’opérateur de délai afin que la transition ne se produise pas trop rapidement.

importer {
  catchError,
  debounceTime,
  retard,
  filtre,
  carte,
  cueillir,
  switchMap,
  robinet
} de 'rxjs / operators';

Et utilisez-le juste avant map (Component):

ajax (url) .pipe (
  plumer ('réponse'),
  délai (1500),
  carte (composant),
  catchError (error => of ())
)

Notre résultat?

Je me demande jusqu'où aller dans ce schéma et dans quelle direction. S'il vous plaît laissez un commentaire et partagez vos pensées!

Et rappelez-vous de tenir ce bouton clap. (Vous pouvez aller jusqu'à 50!)

Jusqu'à la prochaine fois.

Prends soin de toi,
Yazeed Bzadough
http://yazeedb.com/