This translation is a Russian-language interpretation of the documentation, which I myself wrote, so do not hesitate to ask questions.
Handling user input may not be as easy as it sounds. We do not want to send requests to the server while the user is still typing his request? And, of course, the user must always see the result on the last request he sent.
There are different ways to respond to interactive events in React applications, and, in my opinion, the reactive approach (thanks to such libraries as RxJS or Bacon) is one of the best. That's just to use RxJS and React at the same time, you have to deal with the life cycle of the React component, manually manage subscriptions to streams, and so on. The good news is that all this can be done automatically using RxConnect, a library developed during the migration process from Angular to React to ZeroTurnaround.
First was React. And it was good.
... But then people realized that making API requests and scattering the state of the application across different components is not good. And there was a Flux architecture. And it became good.
... But then people realized that instead of having a lot of data storage, there could be one. And Redux appeared. And it became good and centralized.
But another problem has appeared - it has become difficult to do simple things, and every sneeze (such as the login field) must pass through the action creators, reducer, and be stored in a global state. And then everyone remembered that the React component ... may have a local state! How well Dan noticed :
Use the state of the React component where it is unimportant for the global state of the application and when it (the local state) does not change through complex transformations. For example, checkbox status, form field.
Use Redux as a state storage for a global state or for a state that is modified through complex transformations. For example, user cache, or draft article entered by the user.
In other words, do what seems least strange (unacceptable).
And RxJS is better suited than ever to manage this local state.
Consider an example:
We will write the simplest timer showing how many seconds have passed, without RxJS or other libraries:
class Timer extends React.Component { state = { counter: 0 } componentWillMount() { setInterval( () => this.setState(state => ({ counter: state.counter + 1 })), 1000 ) } render() { return <div>{ this.state.counter }</div> } }
Simple, isn't it? That's just bad luck - what happens when we remove this component from the scene? It will continue to call setState()
and throw an exception, because you cannot call setState()
on remote components.
So, we need to make sure that we unsubscribe from the interval before the component is removed:
class Timer extends React.Component { state = { value: 0 } intervalRef = undefined; componentWillMount() { this.intervalRef = setInterval( () => this.setState(state => ({ value: state.value + 1 })), 1000 ) } componentWillUnmount() { clearInterval(this.intervalRef); } render() { return <div>{ this.state.value }</div> } }
This problem is so popular that there is even a library for this: https://github.com/reactjs/react-timer-mixin
Now imagine that for each Promise, interval, and any other asynchronous, we have to write our own signing and unsubscribing handlers, separate libraries. Well, there is RxJS - an abstraction that allows you to handle such things reactively .
The same example, but using RxJS alone, will look something like this:
class Timer extends React.Component { state = { value: 0 } subscription = undefined; componentWillMount() { this.subscription = Rx.Observable.timer(0, 1000).timestamp().subscribe(::this.setState); } componentWillUnmount() { this.subscription.dispose(); } render() { return <div>{ this.state.value }</div> } }
But is it a bit too much code for such a simple task? And what if the developer forgets to call dispose on the subscription? And, since we already have a state in the form of Rx.Observable.timer
, why should we duplicate it in the form of a local component state?
This is where RxConnect will help us:
import { rxConnect } from "rx-connect"; @rxConnect( Rx.Observable.timer(0, 1000).timestamp() ) class Timer extends React.PureComponent { render() { return <div>{ this.props.value }</div> } }
(You can play with an example at http://codepen.io/bsideup/pen/wzvGAE )
RxConnect is implemented as a Higher Order Component and takes over the entire subscription management routine, which makes your code safer and improves readability. The same component is now a function of properties, i.e. "Pure", which greatly simplifies testing due to lack of internal state.
And starting with React 0.14, we can use functions as React stateless components, due to which the code can be turned into one line:
const Timer = rxConnect(Rx.Observable.timer(0, 1000).timestamp())(({ value }) => <div>{value}</div>)
True, I find the class option much more readable.
Timers are good, but most often we have to deal with boring API and all sorts of services, so let's look at a more realistic example - search for articles from Wikipedia.
Let's start with the component itself:
class MyView extends React.PureComponent { render() { const { articles, search } = this.props; return ( <div> <label> Wiki search: <input type="text" onChange={ e => search(e.target.value) } /> </label> { articles && ( <ul> { articles.map(({ title, url }) => ( <li><a href={url}>{title}</a></li> ) ) } </ul> ) } </div> ); } }
As you can see, he expects two properties:
The component is clean and stateless. Remember it, because we will no longer modify its code!
Note: RxConnect works with existing React components without modifications.
It's time to connect our component with the outside world:
import { rxConnect } from "rx-connect"; @rxConnect(Rx.Observable.of({ articles: [ { title: "Pure (programming Language)", url: "https://en.wikipedia.org/wiki/Pure_(programming_language)" }, { title: "Reactive programming", url: "https://en.wikipedia.org/wiki/Reactive_programming" }, ] })) class MyView extends React.PureComponent { // ... }
(Play around: http://codepen.io/bsideup/pen/VKwKGv )
Here we simulated data by laying a static array of two elements, and we see that the component from displays! Hooray!
** Note: the function passed to the rxConnect
method should return Observable
component properties.
All of course cool, but ... Search? The user is still unable to interact with our component. Requirements were:
Thanks to RxJS, we can easily implement this:
import { rxConnect, ofActions } from "rx-connect"; function searchWikipedia(search) { return Rx.DOM .jsonpRequest(`https://en.wikipedia.org/w/api.php?action=opensearch&search=${search}&format=json&callback=JSONPCallback`) .pluck("response") // Wikipedia o_O .map(([,titles,,urls]) => titles.map((title, i) => ({ title, url: urls[i] }))) } @rxConnect(() => { const actions = { search$: new Rx.Subject() } const articles$ = actions.search$ .pluck(0) // .flatMapLatest(searchWikipedia) return Rx.Observable.merge( Rx.Observable::ofActions(actions), articles$.map(articles => ({ articles })) ) }) class MyView extends React.PureComponent { // ... }
(Play: http://codepen.io/bsideup/pen/rrNrEo ATTENTION! Do not enter too quickly, otherwise you will rest against the restriction of the API by the number of requests (see below))
Great, it works! We print and we see the result.
Go through the code step by step:
const actions = { search$: new Rx.Subject() }
Here we create an object from user actions. They are Subjects . You can declare as many subjects as you want.
See the $ sign at the end of the action name? This is a special notation in RxJS to identify the data stream. RxConnect will drop it, and the component will receive it as a search
property.
But the actions themselves do nothing, we must respond to them with the help of reactions:
const articles$ = actions.search$ .pluck(0) // select first passed argument .flatMapLatest(searchWikipedia)
Now we have only one reaction - to search, but there may be many, which is why we combine the flows of all reactions into one:
return Rx.Observable.merge( Rx.Observable::ofActions(actions), articles$.map(articles => ({ articles })) )
The stream of articles will be converted to the articles
property of our component.
In the current implementation, we request the API each time the user enters a new character in the input field. This means that if the user enters too often, for example, 10 characters per second, then we will send 10 requests per second. But the user wants to see only the result for the last query, when he stopped typing. And this situation is a great example for which we choose RxJS - because it is designed to handle such situations!
We modify our reaction a bit:
actions.search$ .debounce(500) // <-- RxJS ! .pluck(0) .flatMapLatest(searchWikipedia)
(Play: http://codepen.io/bsideup/pen/gwOLdK (do not be afraid to enter as fast as you can)
Now the user can enter at any speed, because we will send a request only when there is no input from the user within 500ms, which means our server will receive a maximum of 2 requests per second.
To the note: study RxJS, it is gorgeous :)
Type something in the input box. After seeing the results, enter something else. Old results remain on the screen until we receive a response from the server to a new request. It's not very beautiful, but we can fix it easily.
Remember I said that we combine data streams, which means our component is reactive? Due to this, it is not more difficult to clean up previous results than to send an empty object at the same time as we send a request to the server, but before its result:
actions.search$ .debounce(500) .pluck(0) .flatMapLatest(search => searchWikipedia(search) .startWith(undefined) // <-- undefined, , , )
Result: http://codepen.io/bsideup/pen/mAbaom
In order to reduce the size of the article, I will not cover the topic of Redux, I will just say that RxConnect works great with Redux and allows you to also reactively link your components, replacing @connect
. For example:
@rxConnect((props$, state$, dispatch) => { const actions = { logout: () => dispatch(logout()), }; const user$ = state$.pluck("user").distinctUntilChanged(); return Rx.Observable.merge( Rx.Observable::ofActions(actions), user$.map(user => ({ user })), ); }) export default class MainView extends React.PureComponent { // ... }
Example: https://github.com/bsideup/rx-connect/tree/master/examples/blog/
Demo: https://bsideup.imtqy.com/rx-connect/examples/blog/dist/
Reactive programming can be easier than it seems. After we transferred most of our components to RxJS, we no longer see any other way. And RxConnect allowed us to avoid unnecessary code and potential subscription management errors.
Source: https://habr.com/ru/post/309226/
All Articles