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

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:
– if login fails, show an error
– if login succeeds, wait 2 seconds and then show a system message

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

  • Very easy to understand
  • Uses familiar flow control constructs
  • Logic is all in one place

Cons

  • Unit testing is more complicated
    • you need to mock dispatch and rewire postLogin
    • takes longer when your thunk contains delays
    • asynchronous tests
  • There is no clean/easy/etc way to cancel an in-progress thunk
  • No longer dispatching plain action objects

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 = postLogin
export 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

  • Easy to understand
  • Can use promises

Cons

  • Only works for trivial cases
  • Can’t chain operations together
  • No way to cancel a promise
  • No longer dispatching plain action objects

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

  • Easy to understand
  • Easy to test
  • Excellent documentation
  • Uses familiar flow control constructs
  • Logic is all in one place
  • Built-in support for cancellation
  • Supports very complex operations

Cons

  • Unit testing requires intimate knowledge of the implementation of the saga
  • Requires generator support (TypeScript 2.3, Babel, Node with –harmony, etc)
  • Debugging is difficult

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

  • Logic is all in one place
  • Supports cancellation
  • Can pre-process actions
  • Doesn’t use generators
  • Very good documentation

Cons

  • like thunks, difficult to test
  • Maybe trying to do too much? Swiss Army Knife of middleware. But perhaps that is a pro for some…

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 action
export const loginPromise = (username, password) => {
    return postLogin(username, password).then(
        loginSuccess,
        loginFailure
    );
};

// Helper function to show a message after a delay
export 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

  • Logic is closer to the reducers
  • Easy to test

Cons

  • Makes reducers more complicated
    • Violation of single responsibility principle
    • Reducers are now responsible for two things
    • Two types of boilerplate interleaved instead of just one
  • Very difficult to follow the logic for a non-trivial sequence
  • Doesn’t appear to have a way to cancel a loop
  • Logic is scattered in many places

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 reducer
export 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

  • Cleaner/simpler implementation than redux-loop
  • Elegant solution
  • Easy to test

Cons

  • Like redux-loop, logic is spread out over many places
  • No easy way to cancel an operation.
    • Could store cancellation request in state and then check it in many places (Ugh)
  • Requires generator support (or transpiler)

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 defined
export 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 effects
export 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

  • Type-safe effects via yield*
  • Uses familiar flow control constructs
  • Logic is all in one place
  • Easy to test

Cons

  • Over-engineered? Too much abstraction?
    • More verbose than redux-saga and fewer features
  • Doesn’t leverage middleware to execute side effects (have to write code instead)
  • Effect runner and control would quickly get unmanageable in a non-trivial app
    • But you can break them into sub functions and compose them

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

  • Built-in support for cancellation
  • Supports very complex operations
  • Large Reactive community

Cons

  • Difficult to understand unless you’re already familiar with Reactive
  • Grows very complicated with complex operations
  • Debugging is difficult
  • Testing is difficult

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

  • Type-checker friendliness
  • Declarative side effects
  • Support for cancellation

Cons

  • Steep learning curve
  • Difficult to debug
  • More verbose than redux-observable

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

  • Uses simple, familiar constructs

Cons

  • Documentation needs more introductory examples
  • Trees of complex effects will be difficult to manage
  • Write your own middleware to handle side effects

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?

Want Modern web development to be easier?

I've got lots of tools, tips, best practices, and code to share with you.
Sign up on my list to be notified when new posts are published.

(100% spam free and one-click unsubscribe)

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

2 thoughts on “What is the right way to do asynchronous operations in Redux?

    1. Philip Davis says:

      Thanks! I haven’t used redux-query. It looks like it’s designed for communicating with your server, which is orthogonal to a discussion about how to handle asynchronous operations in general. It would be more apt to compare it to the likes of Falcor, GraphQL, or ‘naked’ libraries like fetch, axios, etc.

Comments are closed.