AJAX Polling in Redux Part 2: Sagas

A while back I wrote a small piece on AJAX polling in React, or how to control request timings when you need to periodically send requests on a set interval. I showed that it's technically possible to do with vanilla React and Redux by utilizing component lifecycle methods. But while this approach works I found that it required careful attention to how props are filtered and managed within componentWillReceiveProps. Ultimately my goal is to keep async logic out of components as much as possible.

There are various options now in the Redux ecosystem for managing side effects, from the very basic redux-thunk, to the Elm-inspired redux-loop, all the way to redux-saga which is powered by generators.

Ideally I like to centralize all async requests in an API middleware, such as the one exemplified in the Redux real-world example. Using thunks will wind up polluting my action creators with async logic, so they're out of the question right now. Using redux-loop would also conflict with my middleware since as a store enhancer it modifies the signature of the store, which would require modifications to any downstream middlewares. So I decided to explore redux-saga, which essentially allows me to daemonize various tasks in the background of my application. This approach lets me retain my centralized async logic via middleware while setting up various "watchers" in order to trigger side effects. How might this look for AJAX polling?

// Utility function to delay effects
function delay(millis) {  
    const promise = new Promise(resolve => {
        setTimeout(() => resolve(true), millis)
    });
    return promise;
}

// Fetch data every 20 seconds                                           
function* pollData() {  
    try {
        yield call(delay, 20000);
        yield put(dataFetch());
    } catch (error) {
        // cancellation error -- can handle this if you wish
        return;
    }
}

// Wait for successful response, then fire another request
// Cancel polling if user logs out                                         
function* watchPollData() {  
    while (true) {             
        yield take(DATA_FETCH_SUCCESS);
        yield race([
            call(pollData),
            take(USER_LOGOUT)
        ]);
    }
}

// Daemonize tasks in parallel                       
export default function* root() {  
    yield [
        fork(watchPollData)
        // other watchers here
    ];
}

This polling logic can exist as sagas without having to deal with potentially complex component lifecycles. I added the race condition for a USER_LOGOUT action type to replace what was previously a clearTimeout inside of componentWillUnmount. This effectively "cancels" the pollData saga if the logout action is dispatched.

The missing pieces here are as follows:

dataFetch -- an action creator that dispatches an action to be handled by API middleware. The middleware is where requests are actually made, and subsequent actions are fired as a result of those requests.

watchPollData -- the saga that runs during the lifetime of the application. It is suspended until it receives a DATA_FETCH_SUCCESS action type, after which it calls the pollData saga.

pollData -- suspends the generator for 20 seconds before dispatching the action generated by dataFetch.

The various effects in use here, take, put, race, call, and fork, are all described in the redux-saga documentation.

You can compare this approach with the container approach I outlined in the previous article. Switching to sagas has given me more predictability and centralization for my side effects, with a caveat being that generator support is still a little inconsistent across browsers. If you use ES2015 with Babel then you're likely already polyfilling generator support.

All the data container has to do now is simply call dataFetch() once on mount, and the polling is kicked off automatically by our sagas. Pretty neat.