Πώς να δημιουργήσετε τη λειτουργικότητα αναζήτησης GitHub στο React με RxJS 6 και Recompose

Αυτή η ανάρτηση προορίζεται για άτομα με εμπειρία React και RxJS. Απλώς μοιράζομαι μοτίβα που βρήκα χρήσιμα κατά τη δημιουργία αυτού του περιβάλλοντος εργασίας χρήστη.

Εδώ χτίζουμε:

Δεν υπάρχουν μαθήματα, άγκιστρα κύκλου ζωής ή setState.

Ρύθμιση

Όλα είναι στο GitHub μου.

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

Το masterυποκατάστημα έχει ολοκληρώσει το έργο, οπότε ελέγξτε το startυποκατάστημα εάν θέλετε να ακολουθήσετε.

git checkout start

Και εκτελέστε το έργο.

npm start

Η εφαρμογή θα πρέπει να λειτουργεί localhost:3000και εδώ είναι το αρχικό περιβάλλον χρήστη μας.

Ανοίξτε το έργο στο αγαπημένο σας πρόγραμμα επεξεργασίας κειμένου και δείτε src/index.js.

Ανασυνθέτω

Εάν δεν το έχετε δει ακόμα, το Recompose είναι μια υπέροχη ζώνη χρησιμότητας React για κατασκευή εξαρτημάτων σε λειτουργικό στυλ προγραμματισμού. Έχει πολλές λειτουργίες και δυσκολεύομαι να διαλέξω τα αγαπημένα μου.

Είναι Lodash / Ramda, αλλά για το React. Λατρεύω επίσης ότι υποστηρίζουν παρατηρήσιμα. Παραθέτοντας από τα έγγραφα:

Αποδεικνύεται ότι μεγάλο μέρος του React Component API μπορεί να εκφραστεί ως παρατηρήσιμο

Θα ασκήσουμε αυτήν την ιδέα σήμερα! ;

Ροή του συστατικού μας

Αυτή τη στιγμή Appείναι ένα συνηθισμένο συστατικό React. Μπορούμε να το επιστρέψουμε μέσω ενός παρατηρήσιμου χρησιμοποιώντας τη συνάρτηση Recompose's komponenFromStream.

Αυτή η συνάρτηση αρχικά αποδίδει ένα μηδενικό συστατικό και εκ νέου αποδίδεται όταν το παρατηρήσιμο μας επιστρέφει μια νέα τιμή

Μια παύλα του Config

Οι ροές επανασύνθεσης ακολουθούν την πρόταση παρατηρήσιμου ECMAScript. Περιγράφει πώς πρέπει να λειτουργούν τα παρατηρήσιμα όταν τελικά αποστέλλονται σε σύγχρονα προγράμματα περιήγησης.

Μέχρι να εφαρμοστούν πλήρως, ωστόσο, βασίζουμε σε βιβλιοθήκες όπως RxJS, xstream, most, Flyd και ούτω καθεξής.

Το Recompose δεν ξέρει ποια βιβλιοθήκη χρησιμοποιούμε, επομένως παρέχει ένα setObservableConfigγια να μετατρέψουμε το ES Observables σε / από οτιδήποτε χρειαζόμαστε.

Δημιουργήστε ένα νέο αρχείο με το srcόνομα observableConfig.js.

Και προσθέστε αυτόν τον κωδικό για να κάνετε το Recompose συμβατό με το RxJS 6:

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

Εισαγάγετε το σε index.js:

import './observableConfig'; 

Και είμαστε έτοιμοι!

Ανασύνθεση + RxJS

Εισαγωγή componentFromStream.

import React from 'react'; import ReactDOM from 'react-dom'; import { componentFromStream } from 'recompose'; import './styles.css'; import './observableConfig'; 

Και αρχίστε να επαναπροσδιορίζετε Appμε αυτόν τον κωδικό:

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

Παρατηρήστε ότι componentFromStreamαπαιτείται μια λειτουργία επανάκλησης αναμένοντας prop$ροή Η ιδέα είναι να propsγίνουμε παρατηρήσιμοι, και τους χαρτογραφούμε σε ένα στοιχείο React.

Και αν έχετε χρησιμοποιήσει το RxJS, γνωρίζετε τον τέλειο χειριστή για να χαρτογραφήσετε τιμές.

Χάρτης

Όπως υποδηλώνει το όνομα, μεταμορφώνεστε Observable(something)σε Observable(somethingElse). Στην περίπτωσή μας, Observable(props)σε Observable(component).

Εισαγωγή του mapχειριστή:

import { map } from 'rxjs/operators'; 

Και επαναπροσδιορίστε την εφαρμογή:

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

Από το RxJS 5, χρησιμοποιούμε pipeαντί για αλυσοδεμένους χειριστές.

Αποθηκεύστε και ελέγξτε το περιβάλλον εργασίας σας, το ίδιο αποτέλεσμα!

Προσθήκη ενός χειριστή συμβάντων

Τώρα θα κάνουμε inputλίγο πιο αντιδραστικό.

Εισαγάγετε το createEventHandlerαπό το Recompose.

import { componentFromStream, createEventHandler } from 'recompose'; 

Και χρησιμοποιήστε το έτσι:

const App = componentFromStream((prop$) => { const { handler, stream } = createEventHandler(); return prop$.pipe( map(() => ( {' '} )) ); }); 

createEventHandlerείναι ένα αντικείμενο με δύο ενδιαφέρουσες ιδιότητες: handlerκαι stream.

Κάτω από το καπό, handlerείναι ένας εκπομπός γεγονότος που ωθεί τις τιμές stream, το οποίο είναι μια παρατηρήσιμη μετάδοση αυτών των τιμών στους συνδρομητές της.

Έτσι θα συνδυάσουμε το streamπαρατηρήσιμο και το prop$παρατηρήσιμο για πρόσβαση στην inputτρέχουσα τιμή.

combineLatest είναι μια καλή επιλογή εδώ.

Πρόβλημα με κοτόπουλο και αυγό

To use combineLatest, though, both stream and prop$ must emit. stream won’t emit until prop$ emits, and vice versa.

We can fix that by giving stream an initial value.

Import RxJS’s startWith operator:

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

And create a new variable to capture the modified stream.

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

We know that stream will emit events from input's onChange, so let’s immediately map each event to its text value.

On top of that, we’ll initialize value$ as an empty string — an appropriate default for an empty input.

Combining It All

We’re ready to combine these two streams and import combineLatest as a creation method, not as an operator.

import { combineLatest } from 'rxjs'; 

You can also import the tap operator to inspect values as they come:

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

And use it like 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(() => ( )) ); }); 

Now as you type, [props, value] is logged.

User Component

This component will be responsible for fetching/displaying the username we give it. It’ll receive the value from App and map it to an AJAX call.

JSX/CSS

It’s all based off this awesome GitHub Cards project. Most of the stuff, especially the styles, is copy/pasted or reworked to fit with React and props.

Create a folder src/User, and put this code into User.css:

And this code into src/User/Component.js:

The component just fills out a template with GitHub API’s standard JSON response.

The Container

Now that the “dumb” component’s out of the way, let’s do the “smart” component:

Here’s 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) =>

{user}

) ); return getUser$; }); export default User;

We define User as a componentFromStream, which returns a prop$ stream that maps to an

.

debounceTime

Since User will receive its props through the keyboard, we don’t want to listen to every single emission.

When the user begins typing, debounceTime(1000) skips all emissions for 1 second. This pattern’s commonly employed in type-aheads.

pluck

This component expects prop.user at some point. pluck grabs user, so we don’t need to destructure our props every time.

filter

Ensures that user exists and isn’t an empty string.

map

For now, just put user inside an

tag.

Hooking It Up

Back in src/index.js, import the User component:

import User from './User';

And provide value as the user prop:

return combineLatest(prop$, value$).pipe( tap(console.warn), map(([props, value]) => ( {' '} )) ); 

Now your value’s rendered to the screen after 1 second.

Good start, but we need to actually fetch the user.

Fetching the User

GitHub’s User API is available here. We can easily extract that into a helper function inside User/index.js:

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

Now we can add map(formatUrl) after filter:

You’ll notice the API endpoint is rendered to the screen after 1 second now:

But we need to make an API request! Here comes switchMap and ajax.

switchMap

Also used in type-aheads, switchMap’s great for literally switching from one observable to another.

Let’s say the user enters a username, and we fetch it inside switchMap.

What happens if the user enters something new before the result comes back? Do we care about the previous API response?

Nope.

switchMap will cancel that previous fetch and focus on the current one.

ajax

RxJS provides its own implementation of ajax that works great with switchMap!

Using Them

Let’s import both. My code is looking like this:

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

And use them like 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$; }); 

Switch from our input stream to an ajax request stream. Once the request completes, grab its response and map to our User component.

We’ve got a result!

Error handling

Try entering a username that doesn’t exist.

Even if you change it, our app’s broken. You must refresh to fetch more users.

That’s a bad user experience, right?

catchError

With the catchError operator, we can render a reasonable response to the screen instead of silently breaking.

Import it:

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

And stick it to the end of your ajax chain.

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

At least we get some feedback, but we can do better.

An Error Component

Create a new component, src/Error/index.js.

import React from 'react'; const Error = ({ response, status }) => ( 

Oops!

{status}: {response.message}

Please try searching again.

); export default Error;

This will nicely display response and status from our AJAX call.

Let’s import it in User/index.js:

import Error from '../Error'; 

And of from RxJS:

import { of } from 'rxjs'; 

Remember, our componentFromStream callback must return an observable. We can achieve that with of.

Here’s the new code:

ajax(url).pipe( pluck('response'), map(Component), catchError((error) => of()) ); 

Simply spread the error object as props on our component.

Now if we check our UI:

Much better!

A Loading Indicator

Normally, we’d now require some form of state management. How else does one build a loading indicator?

But before reaching for setState, let’s see if RxJS can help us out.

The Recompose docs got me thinking in this direction:

Instead of setState(), combine multiple streams together.

Edit: I initially used BehaviorSubjects, but Matti Lankinen responded with a brilliant way to simplify this code. Thank you Matti!

Import the merge operator.

import { merge, of } from 'rxjs'; 

Όταν υποβληθεί το αίτημα, θα συγχωνευτούμε ajaxμε μια ροή στοιχείων φόρτωσης.

Μέσα componentFromStream:

const User = componentFromStream((prop$) => { const loading$ = of(

Loading...

); // ... });

Ένας απλός h3δείκτης φόρτωσης μετατράπηκε σε παρατηρήσιμο! Και χρησιμοποιήστε το έτσι:

const loading$ = of(

Loading...

); const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter((user) => user && user.length), map(formatUrl), switchMap((url) => merge( loading$, ajax(url).pipe( pluck('response'), map(Component), catchError((error) => of()) ) ) ) );

Λατρεύω πόσο συνοπτικό είναι αυτό. Κατά την είσοδο switchMap, συγχωνεύστε το loading$και ajaxπαρατηρήσιμα.

Δεδομένου ότι loading$είναι μια στατική τιμή, θα εκπέμπει πρώτα. ajaxΩστόσο, μόλις ολοκληρωθεί το ασύγχρονο , θα εκπέμπεται και θα εμφανίζεται στην οθόνη.

Πριν από τη δοκιμή, μπορούμε να εισαγάγουμε τον delayτελεστή ώστε η μετάβαση να μην γίνει πολύ γρήγορα.

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

Και χρησιμοποιήστε το λίγο πριν map(Component):

ajax(url).pipe( pluck('response'), delay(1500), map(Component), catchError((error) => of()) ); 

Το αποτέλεσμα μας;

Αναρωτιέμαι πόσο μακριά να ακολουθήσω αυτό το μοτίβο και σε ποια κατεύθυνση. Παρακαλώ μοιραστείτε τις σκέψεις σας!