So führen Sie Benutzersuchen auf GitHub mit React + RxJS 6 + Recompose durch

Ein Bild, um Aufmerksamkeit zu erregen


Dieser Artikel richtet sich an Personen mit Erfahrung mit React und RxJS. Ich teile nur Vorlagen, die ich für das Erstellen einer solchen Benutzeroberfläche nützlich fand.


Folgendes machen wir:



Arbeiten ohne Klassen mit einem Lebenszyklus oder setState .


Vorbereitung


Alles was du brauchst ist in meinem Repository auf GitHub.


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

In der Hauptniederlassung befindet sich ein fertiges Projekt. Wechseln Sie zum Startzweig, wenn Sie Schritt für Schritt fortfahren möchten.


 git checkout start 

Und führen Sie das Projekt aus.


 npm start 

Die Anwendung sollte bei localhost:3000 starten und hier ist unsere erste Benutzeroberfläche.



Starten Sie Ihren bevorzugten Editor und öffnen Sie die src/index.js .



Neu komponieren


Wenn Sie mit Recompose nicht vertraut sind, ist dies eine wunderbare Bibliothek, mit der Sie React-Komponenten in einem funktionalen Stil erstellen können. Es enthält eine Vielzahl von Funktionen. Hier sind meine Favoriten .


Es ist wie Lodash / Ramda, nur reagieren.


Ich bin auch sehr froh, dass es das Observer-Muster unterstützt. Zitieren der Dokumentation :


Es stellt sich heraus, dass der größte Teil der React Component-API in Form des Observer-Musters ausgedrückt werden kann.

Heute werden wir dieses Konzept üben!


Inline-Komponente


Bisher haben wir die App - die häufigste React-Komponente. Mit der Funktion componentFromStream aus der Recompose-Bibliothek können wir sie über ein beobachtbares Objekt abrufen.


Die Funktion componentFromStream startet das Rendern für jeden neuen Wert von unserem Observable aus. Wenn noch keine Werte vorhanden sind, wird null .


Konfiguration


Streams in Recompose folgen dem Dokument ECMAScript Observable Proposal . Es wird beschrieben, wie Observable-Objekte funktionieren sollten, wenn sie in modernen Browsern implementiert sind.


In der Zwischenzeit werden wir Bibliotheken wie RxJS, xstream, most, Flyd usw. verwenden.


Recompose weiß nicht, welche Bibliothek wir verwenden, daher bietet es die Funktion setObservableConfig . Damit können Sie alles, was wir brauchen, in ein ES Observable konvertieren.


Erstellen Sie eine neue Datei im Ordner src und nennen Sie sie observableConfig.js .


Um RxJS 6 mit Recompose zu verbinden, schreiben Sie den folgenden Code hinein:


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

Importieren Sie diese Datei in index.js :


 import './observableConfig'; 

Das ist alles!


+ RxJS neu zusammensetzen


Fügen Sie den componentFromStream Import zu index.js :


 import { componentFromStream } from 'recompose'; 

Beginnen wir mit dem Überschreiben der App Komponente:


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

Beachten Sie, dass componentFromStream eine Funktion mit dem Parameter prop$ als Argument verwendet, bei dem es sich um eine beobachtbare Version von props . Die Idee ist, mithilfe der Karte normale props in React-Komponenten umzuwandeln.


Wenn Sie RxJS verwendet haben, sollten Sie mit dem Kartenoperator vertraut sein.


Karte


Wie der Name schon sagt, verwandelt die Karte Observable(something) in Observable(somethingElse) . In unserem Fall - Observable(props) in Observable(component) .


Importieren Sie den map :


 import { map } from 'rxjs/operators'; 

Ergänzen Sie unsere App Komponente:


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

Bei RxJS 5 verwenden wir pipe anstelle einer Anweisungskette.


Speichern Sie die Datei und überprüfen Sie das Ergebnis. Nichts hat sich geändert!



Fügen Sie einen Ereignishandler hinzu


Jetzt werden wir unser Eingabefeld etwas reaktiv machen.


Fügen Sie den createEventHandler Import hinzu:


 import { componentFromStream, createEventHandler } from 'recompose'; 

Wir werden es so verwenden:


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

Das von createEventHandler erstellte Objekt createEventHandler über zwei interessante Felder: handler und stream .


Unter der Haube ist der handler ein Ereignisemitter, der Werte an den stream überträgt. Und stream wiederum ein beobachtbares Objekt, das Werte an Abonnenten weitergibt.


Wir werden stream und prop$ verknüpfen, um den aktuellen Wert des Eingabefelds zu erhalten.


In unserem Fall ist die Verwendung der Funktion combineLatest eine gute Wahl.


Ei und Huhn Problem


Um combineLatest verwenden zu combineLatest , müssen sowohl stream als auch prop$ Werte erzeugen. Aber stream wird nichts veröffentlichen, bis ein Wert prop$ und umgekehrt.


Sie können dies beheben, indem Sie stream Anfangswert setzen.


Importieren Sie die startWith-Anweisung aus RxJS:


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

Erstellen Sie eine neue Variable, um den Wert aus dem aktualisierten stream abzurufen:


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

Wir wissen, dass der stream Ereignisse auslöst, wenn sich das Eingabefeld ändert. Lassen Sie uns sie daher sofort in Text übersetzen.


Und da der Standardwert für das Eingabefeld eine leere Zeichenfolge ist, initialisieren Sie das value$ -Objekt mit dem value$ ''


Zusammen stricken


Jetzt können wir beide Streams verbinden. Importieren Sie combineLatest als Methode zum Erstellen von Observable-Objekten, nicht als Operator .


 import { combineLatest } from 'rxjs'; 

Sie können auch eine tap Anweisung importieren, um Eingabewerte zu überprüfen.


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

Verwenden Sie es so:


 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> )) ) }); 

Wenn Sie jetzt etwas in unser Eingabefeld eingeben, werden die Werte [props, value] in der Konsole angezeigt.



Benutzerkomponente


Diese Komponente ist für die Anzeige des Benutzers verantwortlich, dessen Namen wir an ihn übertragen. Es erhält einen value von der App Komponente und übersetzt ihn in eine AJAX-Anfrage.


Jsx / css


All dies basiert auf dem wunderbaren GitHub Cards- Projekt. Der meiste Code, insbesondere Stile, wird kopiert oder angepasst.


Erstellen Sie den Ordner src/User . Erstellen Sie eine User.css Datei und kopieren Sie diesen Code hinein.


Kopieren Sie diesen Code in die Datei src/User/Component.js .


Diese Komponente füllt die Vorlage einfach mit Daten aus einem Aufruf der GitHub-API.


Behälter


Jetzt ist diese Komponente "dumm" und wir sind nicht unterwegs. Machen wir eine "intelligente" Komponente.


Hier ist 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; 

Wir haben User als componentFromStream definiert, das ein Observable prop$ -Objekt zurückgibt, das eingehende Eigenschaften in <h3> konvertiert.


debounceTime


Unser User erhält jedes Mal neue Werte, wenn eine Taste auf der Tastatur gedrückt wird. Dieses Verhalten ist jedoch nicht erforderlich.


Wenn der Benutzer mit der Eingabe beginnt, überspringt debounceTime(1000) alle Ereignisse, die weniger als eine Sekunde dauern.


zupfen


Wir erwarten, dass das user als props.user . Der Zupfoperator nimmt das angegebene Feld aus dem Objekt und gibt seinen Wert zurück.


Filter


Hier stellen wir sicher, dass der user und keine leere Zeichenfolge ist.


Karte


Wir machen das <h3> -Tag vom user .


Verbinden


src/index.js zurück zu src/index.js und importieren Sie die User :


 import User from './User'; 

Wir übergeben den value als user :


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

Jetzt wird unser Wert mit einer Verzögerung von einer Sekunde angezeigt.



Nicht schlecht, jetzt müssen wir Informationen über den Benutzer erhalten.


Datenanforderung


GitHub bietet eine API zum Abrufen von Benutzerinformationen: https://api.github.com/users/${user} . Wir können leicht eine Hilfsfunktion schreiben:


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

Und jetzt können wir nach dem filter eine map(formatUrl) hinzufügen:


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

Und jetzt wird anstelle des Benutzernamens die URL auf dem Bildschirm angezeigt.


Wir müssen eine Anfrage stellen! switchMap und switchMap kommen zur switchMap .


switchMap


Dieser Operator ist ideal zum Umschalten zwischen mehreren beobachtbaren.


switchMap , der Benutzer hat den Namen eingegeben, und wir werden eine Anfrage in switchMap .


Was passiert, wenn der Benutzer etwas eingibt, bevor die Antwort von der API eintrifft? Sollten wir uns über frühere Anfragen Sorgen machen?


Nein.


Die switchMap die alte Anforderung ab und wechselt zur neuen.


Ajax


RxJS bietet eine eigene ajax Implementierung, die hervorragend mit switchMap !


Versuchen Sie es


Wir importieren beide Operatoren. Mein Code sieht folgendermaßen aus:


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

Und benutze sie so:


 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$; }); 

Die switchMap wechselt von unserem Eingabefeld zu einer AJAX-Anforderung. Wenn eine Antwort eintrifft, wird sie an unsere dumme Komponente weitergeleitet.


Und hier ist das Ergebnis!



Fehlerbehandlung


Versuchen Sie, einen nicht vorhandenen Benutzernamen einzugeben.



Unsere Anwendung ist kaputt.


catchError


Mit dem Operator catchError können wir eine vernünftige Antwort anzeigen, anstatt leise zu catchError .


Wir importieren:


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

Und fügen Sie es am Ende unserer AJAX-Anfrage ein:


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


Schon nicht schlecht, aber natürlich kann man es besser machen.


Komponentenfehler


Erstellen Sie die src/Error/index.js mit dem Inhalt:


 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; 

Es wird die response und den status unserer AJAX-Anfrage gut anzeigen.


Wir importieren es in User/index.js und gleichzeitig in den Operator of von RxJS:


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

Denken Sie daran, dass die an componentFromStream Funktion beobachtbar zurückgeben sollte. Wir können dies mit dem Operator of erreichen:



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

Jetzt sieht unsere Benutzeroberfläche viel besser aus:



Ladeanzeige


Es ist Zeit, das Staatsmanagement einzuführen. Wie können Sie den Ladeindikator sonst noch implementieren?


Was ist, wenn place setState BehaviorSubject ?


Die Dokumentation zum erneuten Erstellen schlägt Folgendes vor:


Kombinieren Sie anstelle von setState () mehrere Threads

Ok, Sie benötigen zwei neue Importe:


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

Das BehaviorSubject enthält den Download-Status und wird durch die merge mit der Komponente verknüpft.


Innerhalb der componentFromStream :


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

Ein BehaviorSubject mit einem Anfangswert oder "state" initialisiert. Da wir nichts tun, bis der Benutzer mit der Eingabe von Text beginnt, initialisieren Sie ihn mit false .


Wir werden den loading$ mit dem tap Operator ändern:


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

Wir werden es so verwenden:


 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} />)) ) ) ); 

Unmittelbar vor der switchMap und AJAX-Anforderung übergeben wir true an den switchMap loading$ und false nach der erfolgreichen Antwort.


Und jetzt verbinden wir einfach das loading$ und getUser$ .


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

Bevor wir uns die Arbeit ansehen, können wir die delay importieren, damit die Übergänge nicht zu schnell sind.


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

delay vor map(Component) :


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

Ergebnis?



Alle :)

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


All Articles