How to search for users on GitHub using React + RxJS 6 + Recompose

Original author: Yazeed Bzadough
  • Transfer

Picture to attract attention


This article is intended for people with experience with React and RxJS. I am only sharing templates that I find useful to create such a UI.


Here is what we do:



Without classes, work with a life cycle or setState.


Training


All you need is in my GitHub repository.


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

In the branch masteris the finished project. Switch to the branch startif you want to move in steps.


git checkout start

And run the project.


npm start

The application should start at the address localhost:3000and here is our initial UI.



Launch your favorite editor and open the file src/index.js.



Recompose


If you are not familiar with Recompose yet , this is a wonderful library that allows you to create React components in a functional style. It contains a large set of functions. Here are my favorite ones.


It's like Lodash / Ramda, only for React.


I am also very glad that it supports the Observer pattern. Citing documentation :


It turns out that most of the React Component API can be expressed in terms of the Observer pattern.

Today we will practice with this concept!


Stream component


So far we have the Appmost common React component. Using the function componentFromStreamfrom the Recompose library, we can get it through an observable object.


The function componentFromStreamstarts the render with each new value from our observable. If there are no values ​​yet, it is рендрит null.


Configuration


Recompose streams follow the ECMAScript Observable Proposal document . It describes how Observable objects should work when they are implemented in modern browsers.


For now we will use libraries such as RxJS, xstream, most, Flyd, etc.


Recompose does not know which library we use, so it provides a function setObservableConfig. With it, you can convert all that we need to ES Observable.


Create a new file in the folder srcand name it observableConfig.js.


To connect RxJS 6 to Recompose, write the following code in it:


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

Import this file into index.js:


import'./observableConfig';

C this all!


Recompose + RxJS


Add imports componentFromStreamto index.js:


import { componentFromStream } from'recompose';

Start overriding the component App:


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

Please note that it componentFromStreamtakes as an argument a function with a parameter prop$that is the observable version props. The idea is to use maps to turn ordinary propscomponents into React.


If you used RxJS, you should be familiar with the map operator .


Map


As the name implies, the map turns Observable(something)into Observable(somethingElse). In our case - Observable(props)in Observable(component).


Import an operator map:


import { map } from'rxjs/operators';

Let's add our component App:


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

With RxJS 5, we use pipeinstead of a chain of operators.


Save the file and check the result. Nothing changed!



Adding an event handler


Now we will make our input field a little reactive.


Add import createEventHandler:


import { componentFromStream, createEventHandler } from'recompose';

We will use this:


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

The object created createEventHandlerhas two interesting fields: handlerand stream.


Under the hoodhandler - the source of events (event emiter), which transmits values ​​to stream. And streamin turn, it is an observable object that passes values ​​to subscribers.


We will connect with each other streamand prop$to get the current value of the input field.


In our case, a good choice would be to use the function combineLatest.


The problem of eggs and chicken


What to use combineLatest, and streamand prop$should release values. But streamit will not release anything until some value is released prop$and vice versa.


You can fix this by setting the streaminitial value.


Port the operator startWithfrom RxJS:


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

Create a new variable to get the value from the updated one stream:


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

We know that it streamwill issue events when the input field changes, so let's translate them into text right away.


And since the default value for the input field is an empty string, we initialize the object with a value$value ''.


Tying together


Now we are ready to link both threads. Import combineLatestas an Observable object creation method, not as an operator .


import { combineLatest } from'rxjs';

You can also import an operator tapto examine incoming values.


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

Use it like this:


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

Now, if you start typing something into our input box, values ​​will appear in the console [props, value].



User component


This component will be responsible for displaying the user whose name we will pass on to it. It will receive valuefrom the component Appand translate it into an AJAX request.


JSX / CSS


All of this is based on the great GitHub Cards project . Most of the code, especially styles, are copied or adapted.


Create a folder src/User. Create a file in it User.cssand copy this code into it .


And copy this code to a file src/User/Component.js.


This component simply fills the template with data from the call to the GitHub API.


Container


Now this component is “stupid” and we are not on the way with it, let's make a “smart” component.


Here is 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$;
});
exportdefault User;

We defined Useras componentFromStream, which returns an Observable object prop$converting incoming properties into <h3>.


debounceTime


Ours Userwill receive new values ​​each time you press a key on the keyboard, but we don’t need this behavior.


When the user starts typing, they debounceTime(1000)will skip all events that last less than one second.


pluck


We expect the object to userbe passed as props.user. The pluck operator takes the specified field from the object and returns its value.


filter


Here we will make sure that it is usertransmitted and is not an empty string.


map


Making a usertag <h3>.


We connect


Let's go back to src/index.jsand import the component User:


import User from'./User';

Pass the value valueas a parameter user:


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

Now our value is displayed on the screen with a delay of one second.



Not bad, now we need to get information about the user.


Data request


GitHub provides an API for getting user information: https://api.github.com/users/${user} . We can easily write a helper function:


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

And now we can add map(formatUrl)after filter:


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

And now instead of the username on the screen displays the URL.


We need to make a request! Come to the rescue switchMapand ajax.


switchMap


This operator is ideal for switching between multiple observables.


Let's say the user typed a name, and we will make a request inside switchMap.


What happens if the user enters something else before the response from the API comes? Should we be worried about previous queries?


Not.


The operator switchMapwill cancel the old request and switch to the new one.


ajax


RxJS provides its own implementation ajaxthat works great with switchMap!


We try


We import both operators. My code looks like this:


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

And we use them like this:


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

The operator switchMapswitches from our input field to an AJAX request. When the answer comes, it passes it to our "stupid" component.


And here is the result!



Error processing


Try entering a non-existent username.



Our application is broken.


catchError


With the operator, catchErrorwe can display a sane answer instead of quietly breaking down.


We import:


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

And insert it at the end of our AJAX request:


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


Not bad, but of course you can do better.


Error component


Create a file src/Error/index.jswith the contents:


import React from'react';
constError = ({ response, status }) => (
  <divclassName="error"><h2>Oops!</h2><b>
      {status}: {response.message}
    </b><p>Please try searching again.</p></div>
);
exportdefaultError;

He will display beautifully responseand statusour AJAX request.


We import it into User/index.js, and along with the operator offrom RxJS:


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

Remember that the function passed in componentFromStreammust return observable. We can achieve this using the operator of:



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

Now our UI looks much better:



Load indicator


It's time to introduce state management. How else can a load indicator be implemented?


What if setStatewe use the place BehaviorSubject?


The Recompose documentation suggests the following:


Instead of setState (), combine multiple threads

Ok, you need two new imports:


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

The object BehaviorSubjectwill contain the status of the download, and mergeassociate it with the component.


Inside componentFromStream:


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

The object is BehaviorSubjectinitialized with an initial value, or "state". Once we do nothing, until the user starts typing the text, we initialize it with a value false.


We will change the state loading$using the operator tap:


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

We will use it like this:


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

Immediately before switchMapand AJAX request, we pass in a loading$value true, and after a successful response - false.


And now we just connect  loading$ and  getUser$.


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

Before we look at work, we can import an operator delayso that the transitions are not too fast.


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

Add delaybefore map(Component):


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

Result?



Everything :)


Also popular now: