import React from 'react'; const App = ({ query, onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default App;
query
property is not stored anywhere, the onChangeQuery
function also does not update the state. In the usual approach, such a component is equipped with a local state management system. It looks like this: class App extends React.Component { constructor(props) { super(props); this.state = { query: '', }; } onChangeQuery = query => { this.setState({ query }); }; render() { return ( <div> <h1>React with RxJS</h1> <input type="text" value={this.state.query} onChange={event => this.onChangeQuery(event.target.value) } /> <p>{`http://hn.algolia.com/api/v1/search?query=${ this.state.query }`}</p> </div> ); } } export default App;
App
component, but you, most likely, at some point working on the application, decide to arrange such a component in the form of a HOC that is suitable for reuse.mapPropsStream()
and componentFromStream()
. In this guide, however, we will do everything on our own. import React from 'react'; const withObservableStream = (...) => Component => { return class extends React.Component { componentDidMount() {} componentWillUnmount() {} render() { return ( <Component {...this.props} {...this.state} /> ); } }; }; const App = ({ query, onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default withObservableStream(...)(App);
App
component, we need to install RxJS: npm install rxjs --save
import React from 'react'; import { BehaviorSubject } from 'rxjs'; ... const App = ({ query, onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); const query$ = new BehaviorSubject({ query: 'react' }); export default withObservableStream( query$, { onChangeQuery: value => query$.next({ query: value }), } )(App);
App
component itself does not change. We only passed two arguments to a higher order component. We describe them:query
argument is an observable object that has an initial value but, moreover, produces new values over time (since this is a BehaviorSubject
). Anyone can subscribe to this observable object. Here is what the RxJS documentation says about objects of the BehaviorSubject
type: “One of the variants of the Subject
objects is the BehaviorSubject
object, which uses the concept of“ current value ”. It stores the last value passed to its subscribers, and when a new observer subscribes to it, it immediately receives this “current value” from the BehaviorSubject
object. Such objects are well suited for the presentation of data, new portions of which appear over time. "onChangeQuery()
function, passed through the HOC component to the App
component, is a normal function that passes the next value to the observed object. This function is transferred in the object, since it may be necessary to transfer to the higher-order component several such functions that perform certain actions with the observed objects. const withObservableStream = (observable, triggers) => Component => { return class extends React.Component { componentDidMount() { this.subscription = observable.subscribe(newState => this.setState({ ...newState }), ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { return ( <Component {...this.props} {...this.state} {...triggers} /> ); } }; };
App
component directly receives the onChangeQuery()
function, which directly works with the observed object, passing new values to it.componentDidMount()
life cycle method for signing and the componentDidMount()
method for unsubscribing. Cancellation is needed to prevent memory leaks . In the subscription of the observed object, the function only sends all incoming data from the stream to the local React state storage using the command this.setState()
.App
component, which will eliminate the problem that arises if the higher order component does not set the initial value for the query
property. If this is not done, then, at the beginning of the work, the query
property will be equal to undefined
. Due to this change, this property gets the default value. const App = ({ query = '', onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> );
query
in a higher-order component: const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component { constructor(props) { super(props); this.state = { ...initialState, }; } componentDidMount() { this.subscription = observable.subscribe(newState => this.setState({ ...newState }), ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { return ( <Component {...this.props} {...this.state} {...triggers} /> ); } }; }; const App = ({ query, onChangeQuery }) => ( ... ); export default withObservableStream( query$, { onChangeQuery: value => query$.next({ query: value }), }, { query: '', } )(App);
App
component receives from the HOC, in the form of properties, only the query
state and the onChangeQuery
function for changing the state.App
). That is why I had to use the local state of React as an intermediate layer, which, moreover, is convenient in the sense that it causes re-rendering. If you know another way to achieve the same goals - you can share it in the comments.query
, you can work in the App
component. Later we will use both values, working with them with the help of another observed object. const SUBJECT = { POPULARITY: 'search', DATE: 'search_by_date', }; const App = ({ query = '', subject, onChangeQuery, onSelectSubject, }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <div> {Object.values(SUBJECT).map(value => ( <button key={value} onClick={() => onSelectSubject(value)} type="button" > {value} </button> ))} </div> <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> </div> );
subject
parameter can be used to refine the request when building the URL used to access the API. Namely, materials can be searched based on their popularity or at the date of publication. Next, create another observable object that can be used to change the subject
parameter. This observable object can be used to organize communication between the App
component and the higher order component. Otherwise, the properties passed to the App
component will not work. import React from 'react'; import { BehaviorSubject, combineLatest } from 'rxjs/index'; ... const query$ = new BehaviorSubject({ query: 'react' }); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({ subject, query, })), { onChangeQuery: value => query$.next({ query: value }), onSelectSubject: subject => subject$.next(subject), }, )(App);
onSelectSubject()
trigger is not new. He, by means of the button, can be used to switch between the two states of the subject
. But the observable object, transmitted to a higher-order component, is something new. It uses the combineLatest()
function from RxJS to combine the latest values returned from two (or more) observed flows. After the subscription to the observed object is registered, if any of the values ( query
or subject
) change, the subscriber will receive both values.combineLatest()
function is its last argument. Here you can specify the order of returning the values generated by the observed objects. In our case, we need them to be represented as an object. This will allow, as before, to destructurize them in a higher-order component and write them to the local state of React. Since we already have the necessary structure, we can omit the step of wrapping the object of the observed query
object. ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({ subject, query, })), { onChangeQuery: value => query$.next(value), onSelectSubject: subject => subject$.next(subject), }, )(App);
{ query: '', subject: 'search' }
, as well as all other objects produced by the combined stream of observed objects, are suitable for destructuring them in a higher-order component and for writing the corresponding values to the local React state. After updating the state, as before, rendering is performed. When you launch the updated application, you should be able to change both values using the input field and the button. Changed values affect the URL used to access the API. Even if only one of these values changes, the other value maintains its last state, since the combineLatest()
function always combines the most recent values from the observed streams. npm install axios --save
App
component should output. Here, as the value of the corresponding parameter by default, we use an empty array, acting in the same way as we did with other parameters. ... const App = ({ query = '', subject, stories = [], onChangeQuery, onSelectSubject, }) => ( <div> ... <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> <ul> {stories.map(story => ( <li key={story.objectID}> <a href={story.url || story.story_url}> {story.title || story.story_title} </a> </li> ))} </ul> </div> );
import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { flatMap, map } from 'rxjs/operators'; ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); const fetch$ = combineLatest(subject$, query$).pipe( flatMap(([subject, query]) => axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ...
subject
and query
, since in order to build the URL with which we will access the API for loading data, we need both values. In the pipe()
method of the observed object, we can use the so-called "RxJS operators" to perform certain actions with values. In this case, we are mapping two values that are placed in the query, which axios uses to get the result. We use the flatMap()
operator flatMap()
map()
to access the result of the successfully resolved promise and not the most returned promise. As a result, after subscribing to this new observable object, each time a new value of subject
or query
received from other observed objects in the system, a new query
is executed, and the result is in the subscription function.combineLatest()
function combineLatest()
our disposal; this makes it possible to directly map it to a property named stories
. In the end, this is how this data is already used in the App
component. export default withObservableStream( combineLatest( subject$, query$, fetch$, (subject, query, stories) => ({ subject, query, stories, }), ), { onChangeQuery: value => query$.next(value), onSelectSubject: subject => subject$.next(subject), }, )(App);
query
) or a button is clicked ( subject
), this affects the observed fetch
object, which contains the most recent values from both streams.fetch
object. In addition, we would not want the fetch
to be affected if the value is represented by an empty string. That is why we can extend the observed query
object using the debounce
operator, which allows us to eliminate too frequent query changes. Namely, thanks to this mechanism, a new event is accepted only after a specified time after the previous event. In addition, we use the filter
statement here, which filters out the flow events if the query
string is empty. import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest, timer } from 'rxjs'; import { flatMap, map, debounce, filter } from 'rxjs/operators'; ... const queryForFetch$ = query$.pipe( debounce(() => timer(1000)), filter(query => query !== ''), ); const fetch$ = combineLatest(subject$, queryForFetch$).pipe( flatMap(([subject, query]) => axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ...
debounce
operator does its job during data entry in the field. However, when you click on a button that affects a subject
value, the request must be executed immediately.query
and subject
, which we see when the App
component is displayed for the first time, are not the same as those obtained from the initial values of the observed objects: const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY);
subject
is undefined
, the query
is an empty string. This is due to the fact that we provided these values as default parameters for restructuring the signature of the App
component function in the signature. The reason for this is that we need to wait for the initial request executed by the observed fetch
object. Since I don’t know exactly how to immediately get the values from the observed query
and subject
objects in the higher order component in order to write them to the local state, I decided to set up the initial state for the higher order component again. const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component { constructor(props) { super(props); this.state = { ...initialState, }; } componentDidMount() { this.subscription = observable.subscribe(newState => this.setState({ ...newState }), ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { return ( <Component {...this.props} {...this.state} {...triggers} /> ); } }; };
App
component. ... const App = ({ query, subject, stories, onChangeQuery, onSelectSubject, }) => ( ... ); export default withObservableStream( combineLatest( subject$, query$, fetch$, (subject, query, stories) => ({ subject, query, stories, }), ), { onSelectSubject: subject => subject$.next(subject), onChangeQuery: value => query$.next(value), }, { query: 'react', subject: SUBJECT.POPULARITY, stories: [], }, )(App);
query$
and subject$
. Such an approach is prone to errors, since the initialization of the observed objects and the initial state of a higher-order component share the same values. I would have liked it better if, instead, the initial values would be extracted from the observed objects in the higher order component to set the initial state. Perhaps someone from the readers of this material will be able to share in the comments advice on how to do this.Source: https://habr.com/ru/post/428081/
All Articles