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 ) => { }); }

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 => { 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.

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 ) { const action = yield take ( LOGIN _ REQUEST ); const { username , password } = action . payload ; try { const result = yield call ( postLogin , username , password ); const { user , msg } = result ; yield put ( loginSuccess ( user )); 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 ({ type : Actions . LOGIN _ REQUEST , 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:

export const loginSuccess = ({ user , msg }) => ({ type : ActionType . LOGIN _ SUCCESS , payload : { user , msg }, }); export const loginPromise = ( username , password ) => { return postLogin ( username , password ). then ( loginSuccess , loginFailure ); }; 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' ; 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.

export const loginEffect = ( username , password ) => ({ type : 'login' , username , password , }); export const delayEffect = milliseconds => ({ type : 'delay' , milliseconds , }); 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 ; } } } 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 ); const request$ = credentials$ . map (({ username , password }) => ({ url : `/api/login` , category : 'login' , })); const response$ = sources . HTTP . select ( 'login' ) . flatten (); 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 : [ [ ({ user , msg }) => { type : 'EFFECT_COMPOSE' , payload : { ... loginSuccess ( user ), meta : { steps : [ ] } } }, 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?

Still not sure? You might also enjoy...

Tweet