What is the right way to do asynchronous operations in Redux?

last updated: Feb 11th, 2017

It's understandable why so many newcomers to React+Redux have difficulty wrapping their heads around asynchronous actions. React is simply a view layer. Redux is simply a state management layer. But it takes much more to build a typical app.

These poor developers are given the corner pieces of a puzzle and the rest of the pieces are scattered about on the ground. There's no reference picture to see what the puzzle is supposed to look like -- everyone you ask would show you a different picture anyways.

Even developers experienced in React+Flux have difficulty picking up Redux because now they're told not to put asynchronous calls in their components. Well then, where?

The Good News...

The good news is that Redux gives you a lot of flexibility.

Pretty much the only steadfast rule is that you musn't put asynchronous code in your reducer.

Look... if you really want to put asynchronous code in your reducer, go ahead. The Redux police aren't going to come lock you up. It's your code, after all! But, by doing so, you're simultaneously suffering all the overhead of Redux and sacrificing many of the benefits. And you'll just confuse anyone else who looks at your code. It's better to learn the right way.

The Bad News...

The bad news is that Redux gives you a lot of flexibility.

Redux doesn't care about who, where, when, what, why, and how data comes. Redux is designed to give you a predictable state machine. There is one central store and exactly one entry point into the store.

Asynchronous Operations

First of all, let's define asynchronous operations.

We're talking about any situation where our code has to wait for something else before it can continue. For example, when we're logging in a user, we send the user's credentials to the server and have to wait for a response to approve or reject the user.

We might have some additional business logic like this:

There are lots of libraries that can help you solve this problem. Unfortunately, discovery is a problem in JavaScript niches like React+Redux.

What Are My Options?

As of the time of writing, the most popular third-party Redux side effects libraries according to GitHub stars are:

Name Watchers Stars Forks Issues % Closed
redux-saga 150 6469 464 487 92%
redux-thunk 73 4333 201 85 65%
redux-observable 74 2258 126 84 69%
redux-promise 25 1364 82 24 29%
redux-loop 43 1021 54 66 91%
redux-ship 10 571 8 3 33%
redux-logic 28 487 21 31 65%
redux-effects 5 438 8 6 50%
redux-cycles 12 268 4 11 82%
redux-side-effects 8 154 7 13 62%

Kudos to redux-loop and redux-saga for having a close-rate of 90%+

(Note: of the 41 open redux-saga issues, only 5 are tagged with the bug label)

Apples to Apples

Let's look at how our login functionality could be implemented in each of these libraries.

In all cases, you can assume we've got the following action types:

export const ActionType = {    LOGIN_REQUEST: 'LOGIN_REQUEST',    LOGIN_SUCCESS: 'LOGIN_SUCCESS',    LOGIN_FAILURE: 'LOGIN_FAILURE',    SHOW_MESSAGE: 'SHOW_MESSAGE',};

In most cases you can assume we've got the following basic action creators (they will have slight variations in some of the examples). We accept a little more boilerplate here where it doesn't matter to reduce the boilerplate in the bulk of our code where it does matter.

export const loginRequest = (username, password) => ({    type: ActionType.LOGIN_REQUEST,    payload: { username, password },});export const loginSuccess = user => ({    type: ActionType.LOGIN_SUCCESS,    payload: user,});export const loginFailure = err => ({    type: ActionType.LOGIN_FAILURE,    payload: err,    error: true,});export const showMessage = msg => ({    type: ActionType.SHOW_MESSAGE,    payload: msg,});

You can also assume we've got a function that performs the actual POST request to our API and returns a Promise. (All the examples will also work with minor modifications if you prefer the Node.js callback-style functions).

function postLogin(username, password) {    return new Promise((resolve, reject) => {        // ...XHR, fetch, axios, whatever    });}

Redux-Thunk

I'm going to start with redux-thunk because that's where the official documentation starts and that's where a lot of people start. It's also the easiest to understand.

With redux-thunk, all the logic is wrapped up neatly in a function called a thunk. This function takes a reference to the dispatch function and you execute it by dispatching the thunk the same way you would a simple action.

export const loginThunk = (username, password) => dispatch => {    // Note: no need to pass in username and password to    // the loginRequest action creator because they're already    // in scope and not used in our store.    dispatch(loginRequest());    postLogin(username, password).then(({ user, msg }) =>        dispatch(loginSuccess(user));        setTimeout(() => {            dispatch(showMessage(msg));        }, 2000);    }, err => {        dispatch(loginFailure(err));    });};

To execute the login sequence we dispatch our thunk:

dispatch(loginThunk(username, password));

Pros

Cons

Redux-Promise

Redux-Promise allows you to dispatch JavaScript Promises to your store.

We can either dispatch a promise directly or in the payload property.

// Redundant... could just say loginRequest = postLoginexport const loginRequest = (username, password) =>    postLogin(username, password);;export const loginRequest = (username, password) => ({    type: ActionType.LOGIN_REQUEST,    payload: postLogin(username, password),});

Regardless of which action creator style we choose, however, redux-promise does not allow us to implement our simple login functionality.

In one case the error is silently dropped and in both cases the success path has no way to chain together the rest of our operations.

Pros

Cons

Redux-Saga

Redux-Saga puts all the asynchronous logic in generator functions called sagas:

function* loginSaga() {    while (true) {        // Sleep until a login request action happens        const action = yield take(LOGIN_REQUEST);        const { username, password } = action.payload;        try {            // Login. The actual call is carried out by the middleware            const result = yield call(postLogin, username, password);            const { user, msg } = result;            yield put(loginSuccess(user));            // Wait a bit and show the message            yield call(delay, 2000);            yield put(showMessage(msg));        }        catch (err) {            yield put(loginFailure(err));        }    }}

To run our code, we just need to register the saga with the middleware and we're done.

sagaMiddleware.run(loginSaga);

Pros

Cons

Redux-Logic

Redux-Logic is like a super-charged thunk from redux-thunk but is invoked from middleware similar to how a saga is called in redux-saga.

const loginLogic = createLogic({    // Only run this when we see a LOGIN_REQUEST    type: Actions.LOGIN_REQUEST,    // This function will run when the action is dispatched.    process({ getState, action }, dispatch, done) {        const { username, password } = action.payload;        postLogin(username, password).then(({ user, msg }) => {            dispatch(loginSucceeded(user));            setTimeout(() => {                dispatch(showMessage(msg));            }, 2000);        }, err => {            dispatch(loginFailure(err));        })        .then(done);    }});const logic = [    loginLogic,    // ...];const logicMiddleware = createLogicMiddleware(logic);const store = createStore(    rootReducer,    applyMiddleware(        logicMiddleware    ));

This little code example doesn't do justice to all the capabilities of redux-logic. It has other lifecycle hooks for intercepting actions for validation, transformation, etc.

Pros

Cons

Redux-Loop

Redux-Loop borrows from Elm the premise that the reducer should be responsible for follow-up effects. In other words, given a current state and an action, redux-loop would have your reducer return the next state and side effects from the state transition.

Pretend this is what our reducer normally looks like:

export const reducer = (state = {}, action) => {    switch (action.type) {        case ActionType.LOGIN_REQUEST:            return { pending: true };        case ActionType.LOGIN_SUCCESS:            return { pending: false, user: action.payload };        case ActionType.LOGIN_FAILURE:            return { pending: false, err: action.payload };        case ActionType.SHOW_MESSAGE:            return { ...state, msg: action.payload };    }    return state;};

This is what it looks like with redux loop, note we also have to modify an action creator and add some helper functions:

// Notice we have to pass in the msg so we don't lose it!export const loginSuccess = ({ user, msg }) => ({    type: ActionType.LOGIN_SUCCESS,    payload: { user, msg },});// Helper function to login and resolve/reject to a new actionexport const loginPromise = (username, password) => {    return postLogin(username, password).then(        loginSuccess,        loginFailure    );};// Helper function to show a message after a delayexport const delayMessagePromise = (msg, delay) => {    return new Promise(resolve => {        setTimeout(() => {            resolve(showMessage(msg));        }, delay);    });};export const reducer = (state = {}, action) => {    switch (action.type) {        case ActionType.LOGIN_REQUEST:            const { username, password } = action.payload;            return loop(                { pending: true },                Effect.promise(loginPromise, username, password))            );        case ActionType.LOGIN_SUCCESS:            const { user, msg } = action.payload;            return loop(                { pending: false, user },                Effect.promise(delayMessagePromise, msg, 2000)            );        case ActionType.LOGIN_FAILURE:            return { pending: false, err: action.payload };        case ActionType.SHOW_MESSAGE:            return { ...state, msg: action.payload };    }    return state;};

We don't need to do anything special to execute our asynchronous operations. Just dispatch the request action and off it goes.

dispatch(loginRequest(username, password));

Pros

Cons

Redux-Side-Effects

Redux-Side-Effects is similar to redux-loop in the sense that it believes the reducer should be responsible for describing side effects of an action. However, unlike redux-loop, it uses generator functions as reducers to yield side effects.

import { createStore } from 'redux';import {    createEffectCapableStore, sideEffect} from 'redux-side-effects';// Almost like a thunk, but will be returned by the reducerexport const loginEffect = (dispatch, { username, password }) =>    postLogin(username, password).then(result =>        dispatch(loginSuccess(result)),    err =>        dispatch(loginFailure(err))    );export const showMessageEffect = (dispatch, msg) => {    setTimeout(() => {        dispatch(showMessage(msg));    }, 2000);};const storeFactory = createEffectCapableStore(createStore);const store = storeFactory(function*(state = {}, action) {    switch (action.type) {        case ActionType.LOGIN_REQUEST:            yield sideEffect(loginEffect, action.payload);            return { pending: true };        case ActionType.LOGIN_SUCCESS:            const { user, msg } = action.payload;            yield sideEffect(showMessageEffect, msg);            return { pending: false, user };        case ActionType.LOGIN_FAILURE:            return { pending: false, err: action.payload };        case ActionType.SHOW_MESSAGE:            return { ...state, msg: action.payload };        default:            return state;    }});

Okay, I admit this is actually pretty cool. I could see myself using this on a small/medium project with basic async operations. But I think this will not scale well with a large app and complex operations.

Pros

Cons

Redux-Ship

Redux-Ship aims to have "composable, testable, and typable side effects for Redux." It's twice as verbose as basic Redux because it introduces new concepts, Effect and Commit, as well as new functions to select and run effects.

// Effect creator... looks a lot like an Action Creator, no?export const loginEffect = (username, password) => ({    type: 'login',    username,    password,});export const delayEffect = milliseconds => ({    type: 'delay',    milliseconds,});// This is where the asynchronous operations are definedexport function* control(action) {    switch (action.type) {        case ActionType.LOGIN_REQUEST: {            const { username, password } = action.payload;            try {                const { user, msg } = yield* Ship.call(                    loginEffect(username, password)                );                yield* Ship.commit(loginSuccess(user));                yield* Ship.call(delayEffect(2000));                yield* Ship.commit(showMessage(msg));            }            catch (err) {                yield* Ship.commit(loginFailure(err));            }            return;        }    }}// This function is responsible for performing the side effectsexport const effectRunner = effect => {    switch (effect.type) {        case 'login':            const { username, password } = effect;            return postLogin(username, password);        case 'delay':            return new Promise(resolve => {                setTimeout(resolve, effect.milliseconds);            });    }}export const store = createStore(    applyMiddleware(        Ship.middleware(effectRunner, control)    ));

Overall this is very similar to redux-saga but explicitly puts side effects in the core code rather than letting the middleware take care of them.

Pros

Cons

Redux-Observable

Redux-Observable is radically different from the others because it uses a Reactive-style of programming. It uses the term epic to describe a sequence of asynchronous operations. Everything is a stream. The dollar sign $ is used by convention to indicate a variable that holds a stream.

const loginRequestEpic = action$ =>    action$.ofType(LOGIN_REQUEST)        .mergeMap(({ payload: { username, password } }) =>            Observable.from(postLogin(username, password))                .map(loginSuccess)                .catch(loginFailure)        );const loginSuccessEpic = action$ =>    action$.ofType(LOGIN_SUCCESS)        .delay(2000)        .mergeMap(({ payload: { msg } }) =>            showMessage(msg)        );const rootEpic = combineEpics(    loginRequestEpic,    loginSuccessEpic);const epicMiddleware = createEpicMiddleware(rootEpic);const createStoreWithMiddleware = applyMiddleware(    epicMiddleware)(createStore);const store = createStoreWithMiddleware(initialState);

Pros

Cons

Redux-Cycles

Redux-Cycles, like redux-observable, uses a Reactive style of programming. It attempts to go one step further in making pure functions by eliminating side-effects from the epics. It refers to these pure epics as cycles.

export function loginRequestCycle(sources) {    const credentials$ = sources.ACTION        .filter(action => action.type === LOGIN_REQUEST)        .map(action => action.payload);    // Note: not using our postLogin here.    // Not sure how it fits in with redux-cycles            const request$ = credentials$        .map(({ username, password }) => ({            url: `/api/login`,            category: 'login',            // TODO: indicate POST request            // TODO: send credentials in request body        }));    const response$ = sources.HTTP        .select('login')        .flatten();        // TODO: how to handle errors?    const action$ = response$        .map(loginSuccess);    return {        ACTION: action$,        HTTP: request$,    };}export function loginSuccessCycle(sources) {    const action$ = sources.ACTION        .filter(action => action.type === LOGIN_SUCCESS)        .map(action => action.payload.msg)        .map(msg =>            sources.Time.delay(2000)                .mapTo(showMessage(msg))        )        .flatten();    return {        ACTION: action$,    };}const rootCycle = combineCycles(    loginRequestCycle,    loginSuccessCycle);

Sorry that my example is incomplete. The documentation was a little lacking. I think one is expected to know Reactive methods before using this library.

Pros

Cons

Redux-Effects

Redux-Effects has a novel method of chaining operations by nesting actions with success and failure paths like a decision tree.

export const login = (username, password) => ({    type: 'EFFECT_COMPOSE',    payload: {        ...loginRequest(username, password),        meta: {            steps: [                [                    // Success case first                    ({ user, msg }) => {                        type: 'EFFECT_COMPOSE',                        payload: {                            ...loginSuccess(user),                            meta: {                                steps: [                                    // TODO: how to delay                                    // TODO: showMessage(msg)                                ]                            }                        }                    },                    // Failure case second                    loginFailure                ]            ]        }    }});

I can see this quickly becoming a nightmare. We could manage a little better with a helper method for composition.

But it's unclear to me how we'd model delays and I also don't see a way to cancel a tree of effects because there is no access to the store (although this could be handled via middleware).

Pros

Cons

Summary Comparison

redux-promise and redux-effects were disqualified for not being able to support the basic requirements.

redux-saga redux-thunk redux-observable redux-loop redux-ship redux-logic redux-cycles redux-side-effects
Works in ES5 1 X X X 1 X X 1
Logic in one place X X X X X X
Easy to test X X X X
Supports cancellation X X X X
Imperative code X X X X
Reactive code X X
Advanced features X X X X
Scales with complexity X X

1. Must be transpiled down to ES5 via Babel, TypeScript 2.3, etc.

Conclusion

So what is the right way to do asynchronous operations in Redux?

There really is no right answer for everyone. It's largely a matter of preference.

Beginners should continue to consider redux-thunk first because, as small as Redux is, there's more than enough basics to learn.

Developers who are more comfortable with an imperative, top-down style of programming will do better with redux-saga or redux-thunk. If you like the simplicity of thunks but feel dirty using them, give redux-logic a shot.

Developers who like the Reactive style of programming will feel more comfortable with redux-observable or redux-cycles.

Redux-saga is certainly winning the popularity contest and it (along with redux-logic) scales much better with complexity compared to the other contenders.

Which will you choose?

Still not sure? You might also enjoy...

Tweet

Level up Your React + Redux + TypeScript

with articles, tutorials, sample code, and Q&A.