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