Solution proposed by codeslayer1 in question raised at React - Controlling multiple Ajax Calls has an issue of accessing state directly inside action creator - an anti pattern.
I made a little repo github.com/adz5a/so-stream-example
to illustrate how I would solve your problem.
This repo uses two libraries xstream
and recompose
. The former provides an implementation of ObservableStreams with its operators and the latter wires it up with React.
A concept is necessary before everything : ES Observables
. They are covered in depth in articles such as this (I strongly recommend reading and listening to past articles / talks from Ben Lesh, on this subject).
Observabes are a lazy primitive used to model values over time. In JS we have another primitive for doing async : Promises. Those models an eventual value or error
and thus are not lazy but eager. In the case of a React component ( or more generally UI ) we are interested in lazyness because things can go wrong : the user may want to interrupt a long running process, it can crash, change route etc...
So, how can we solve your problem : controlling a long running process which can be interrupted ( fetching lots of rows ) by user interaction ?
First, the UI :
export class AnswerView extends React.Component {
static propTypes = {
// called when the user make a batch
// of request
onStart: PropTypes.func.isRequired,
// called when you want to stop the processing
// of requests ( when unmounting or at the request
// of the user )
onStop: PropTypes.func.isRequired,
// number of requests completed, 0 by default
completedRequests: PropTypes.number.isRequired,
// whether it's working right now or not
processing: PropTypes.bool.isRequired
};
render () {
// displays a form if no work is being done,
// else the number of completed requests
return (
<section>
<Link to="/other">Change Route !</Link>
<header>
Lazy Component Example
</header>
{
this.props.processing ?
<span>{"requests done " + this.props.completedRequests}<button onClick={this.props.onStop}>Stop !</button></span>:
<form onSubmit={e => {
e.preventDefault();
this.props.onStart(parseInt(e.currentTarget.elements.number.value, 10));
}}>
Nb of posts to fetch<input type="number" name="number" placeholder="0"/>
<input type="submit" value="go"/>
</form>
}
</section>
);
}
componentWillMount () {
console.log("mounting");
}
}
Pretty simple : a form with an input for the number of requests to perform (could checkboxes on a table component ... ).
Its props are as follow :
componentWillUnmout
.This does not do much by itself, so let's introduce recompose
. Its purpose is to enhance component via HOC. We will use the mapPropsStream
helper in this example.
Note : in this answer I use stream / Observable interchangeably but this is not true in the general case. A stream is an Observable with operators
allowing to transform the emitted value into a new Observable.
For a React Component we can sort of observe its props with the standard api : 1st one at componentWillMount, then at componentWillReceiveProps. We can also signal when there will be no more props with componentWillUnmount. We can build the following (marble) diagram : p1--p2--..--pn--|
(the pipe indicates the completion of the stream).
The enhancer code is posted below with comments.
What needs to be understood is that everything with streams can be approached like a signal : by modelling everything as a stream we can be sure that by sending the appropriate signal we can have the desired behaviour.
export const enhance = mapPropsStream(prop$ => {
/*
* createEventHandler will help us generates the callbacks and their
* corresponding streams.
* Each callback invocation will dispatch a value to their corresponding
* stream.
*/
// models the requested number of requests
const { handler: onStart, stream: requestCount$ } = createEventHandler();
// models the *stop* signals
const { handler: onStop, stream: stop$ } = createEventHandler();
// models the number of completed requests
const completedRequestCount$ = requestCount$.map( n => {
// for each request, generate a dummy url list
const urls = Array.from({ length: n }, (_, i) => `https://jsonplaceholder.typicode.com/posts/${i + 1}` );
// this is the trick : we want the process to be aware of itself when
// doing the next operation. This is a circular invocation so we need to
// use a *proxy*. Note : another way is to use a *subject* but they are
// not present in __xstream__, plz look at RxJS for a *subject* overview
// and implementation.
const requestProxy$ = xs.create();
const count$ = requestProxy$
// a *reduce* operation to follow where we are
// it acts like a cursor.
.fold(( n ) => n + 5, 0 )
// this will log the current value
.debug("nb");
const request$ = count$.map( n => Promise.all(urls.slice(n, n + 5).map(u => fetch(u))) )
.map(xs.fromPromise)
.flatten()
.endWhen(xs.merge(
// this stream completes when the stop$ emits
// it also completes when the count is above the urls array length
// and when the prop$ has emitted its last value ( when unmounting )
stop$,
count$.filter(n => n >= urls.length),
prop$.last()
));
// this effectively activates the proxy
requestProxy$.imitate(request$);
return count$;
} )
.flatten();
// models the processing props,
// will emit 2 values : false immediately,
// true when the process starts.
const processing$ = requestCount$.take(1)
.mapTo(true)
.startWith(false);
// combines each streams to generate the props
return xs.combine(
// original props
prop$,
// completed requests, 0 at start
completedRequestCount$.startWith(0),
// boolean indicating if processing is en route
processing$
)
.map(([ props, completedRequests, processing ]) => {
return {
...props,
completedRequests,
processing,
onStart,
onStop
};
})
// allows us to catch any error generated in the streams
// very much equivalent to the new ErrorBoundaries in React
.replaceError( e => {
// logs and return an empty stream which will never emit,
// effectively blocking the component
console.error(e);
return xs.empty();
} );
});
export const Answer = enhance(AnswerView);
I hope this answer is not (too) convoluted, feel free to ask any question.
As a side note, after a little research you may notice that the processing
boolean is not really used in the logic but is merely there to help the UI know what's going on : this is a lot cleaner than having some piece of state attached to the this
of a Component.