For the past few months we’ve been working on the LiveBlog WordPress plugin by Automattic to update the UI to use React + Redux. The LiveBlog plugin uses polling to get the live updates, so we’re going to look into how to implement polling with Redux.
There are two libraries that we looked at to accomplish this; redux-saga and redux-observable. As a very high level overview of them both:
Redux-saga uses ES6 generator functions to make asynchronous flow easy to read. They look similar to the async
/ await
syntax.
Redux-observable is RxJS-based, which is a library for reactive programming using Observables.
Let’s get polling!
To see that polling works, we’re going to use a random joke API, so every time a polling request comes back, we’ll see a new joke appear. What we will accomplish is a poll that runs every 4 seconds, and will wait for the request resolve before starting the poll again. This means if the request more than seconds, the poll will wait and resume once the request has come back.
Redux-Saga
Let’s start with the redux-saga implementation:
/**
* Saga worker.
*/
function* pollSagaWorker(action) {
while (true) {
try {
const { data } = yield call(() => axios({ url: ENDPOINT }));
yield put(getDataSuccessAction(data));
yield call(delay, 4000);
} catch (err) {
yield put(getDataFailureAction(err));
}
}
}
/**
* Saga watcher.
*/
function* pollSagaWatcher() {
while (true) {
yield take(POLL_START);
yield race([
call(pollSagaWorker),
take(POLL_STOP)
]);
}
}
On first glance it looks like there is a lot to digest. Let’s break it down.
The worker
The worker generator function is the main function that performs the API request and delay. As we’re polling, we need to wrap it in a while
loop. This to make it a continuous task and keeps poll running. Without the while
loop, it would only run once and finish.
Inside we use a try
/catch
for handling errors in the request. Let’s take a look at inside the try
block:
const { data } = yield call(() => axios({ url: ENDPOINT }));
yield
in a generator pauses the function, and in this scenario waits for the redux-saga call
effect creator to be resolved before moving on to the next. The call
effect creator instructs the middleware to call the function passed in—in this case—axios. We use axios as it’s more feature rich than the Fetch API and works across all browsers.
yield put(getDataSuccessAction(data));
yield call(delay, 4000);
Next, we use the put
effect creator to dispatch an action to the Redux Store, in this case, a success action with a the data
payload from the response. Once this is complete, it moves on to calling the redux-saga delay
utility function, which returns a promise that resolves after a specified amount of time. This essentially pauses the worker for 4 seconds, creating the poll.
The watcher
To be able to run the poll worker, we need a watcher generator. The watcher will handle the “watching” for a specific action (or actions) and call the worker.
while (true) {
...
}
Again, we start with a while
loop to ensure the task keeps running. Without it, we would only be able to start the poll watcher once, so if the poll gets cancelled, it won’t be able to start again.
yield take(POLL_START);
We use the take
effect creator, which instructs the middleware to wait for an action on the store. Here we wait for the POLL_START
action.
yield race([
call(pollSagaWorker),
take(POLL_STOP)
]);
Once the POLL_START
action has been received, we use the race
effect combinator to either call
the pollSagaWorker
function, or take
the POLL_STOP
action. race
will start a race between multiple effects, and will automatically cancel the losing effect(s). This means if the action POLL_STOP
is received, it will cancel the pollSagaWorker
function.
Here is a live demo:
See the Pen Redux + redux-saga polling by markgoodyear (@markgoodyear) on CodePen.
Redux-Observable
Redux-observable uses Epics to handle Redux side effects. An Epic, as quoted from the documentation, is:
a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.
In terms of polling, the poll start action will be recieved by the Epic, we then perform a request and then dispatch an action with the data from the request.
With that in mind, let’s take a look at the redux-observable implementation:
const pollEpic = action$ =>
action$.ofType(POLL_START)
.switchMap(() =>
Observable.timer(0, 4000)
.takeUntil(action$.ofType(POLL_STOP))
.exhaustMap(() =>
Observable.ajax({ url: ENDPOINT, crossDomain: true })
.map(res => getDataSuccessAction(res.response))
.catch(error => Observable.of(getDataFailureAction(error)))
)
);
This is composed of RxJS Observables and operators. The first thing is setting up the Epic to “listen” for the POLL_START
action. This uses the redux-observable ofType
operator, which is an RxJS filter
operator behind the scenes. The rest of the operators we use are from RxJS directly; switchMap
, exhaustMap
, takeUntil
, map
and catch
, along with three Observables, timer
, of
and ajax
.
A breakdown of what is happening:
- On
POLL_START
, switch to the innertimer
Observable usingswitchMap
. - Run the
timer
Observable until aPOLL_STOP
action is receieved via thetakeUntil
operator. - Switch to the inner
ajax
Observable usingexhaustMap
. We use the RxJSajax
Observable instead of axios, or another promise based solution, as it’s alread an Observable, and provides automatic request cancelation if the Obseravble is cancelled. by usingexhaustMap
, the outer Observable (timer
) will wait on theajax
Observable to complete before resuming. - We then use
map
will then dispatch the success action to the Redux Store, with the request response.
Check out the live example here:
See the Pen Redux + redux-observable polling by markgoodyear (@markgoodyear) on CodePen.
This should give you a firm grasp on how to poll with Redux using either redux-saga or redux-observable. For the LiveBlog we decided to use redux-observable, as being RxJS based, it allows us to utilise RxJS throughout the rest of the plugin. Also, because RxJS isn’t tied into Redux, both code and concepts are portable outside the Redux world. Happy polling!
If you have any comments or questions you can find me on Twitter.