How to handle complex side-effects in Redux?

When you want complex async dependencies, just use Bacon, Rx, channels, sagas, or another asynchronous abstraction. You can use them with or without Redux. Example with Redux:

observeSomething()
  .flatMap(someTransformation)
  .filter(someFilter)
  .map(createActionSomehow)
  .subscribe(store.dispatch);

You can compose your asynchronous actions any way you like—the only important part is that eventually they turn into store.dispatch(action) calls.

Redux Thunk is enough for simple apps, but as your async needs get more sophisticated, you need to use a real asynchronous composition abstraction, and Redux doesn't care which one you use.


Update: Some time has passed, and a few new solutions have emerged. I suggest you to check out Redux Saga which has become a fairly popular solution for async control flow in Redux.


Edit: there now a redux-saga project inspired by these ideas

Here are some nice resources

  • Comparison of Redux-saga and Redux-thunk
  • Redux-saga vs Redux-thunk with async/await
  • Managing processes in Redux Saga
  • From actionsCreators to Sagas
  • Snake game implemented with Redux-saga

Flux / Redux is inspired from backend event stream processing (whatever the name is: eventsourcing, CQRS, CEP, lambda architecture...).

We can compare ActionCreators/Actions of Flux to Commands/Events (terminology usually used in backend systems).

In these backend architectures, we use a pattern that is often called a Saga, or Process Manager. Basically it is a piece in the system that receives events, may manage its own state, and then may issue new commands. To make it simple, it is a bit like implementing IFTTT (If-This-Then-That).

You can implement this with FRP and BaconJS, but you can also implement this on top of Redux reducers.

function submitScoreAfterLoginSaga(action, state = {}) {  
  switch (action.type) {

    case SCORE_RECORDED:
      // We record the score for later use if USER_LOGGED_IN is fired
      state = Object.assign({}, state, {score: action.score}

    case USER_LOGGED_IN: 
      if ( state.score ) {
        // Trigger ActionCreator here to submit that score
        dispatch(sendScore(state.score))
      } 
    break;

  }
  return state;
}

To make it clear: the state computed from reducers to drive React renderings should absolutly stay pure! But not all state of your app has the purpose of triggering renderings, and this is the case here where the need is to synchronise different decoupled parts of your app with complex rules. The state of this "saga" should not trigger a React rendering!

I don't think Redux provide anything to support this pattern but you can probably implement it by yourself easily.

I've done this in our startup framework and this pattern works fine. It permits us to handle IFTTT things like:

  • When user onboarding is active and user close Popup1, then open Popup2, and display some hint tooltip.

  • When user is using mobile website and opens Menu2, then close Menu1

IMPORTANT: if you are using undo/redo/replay features of some frameworks like Redux, it is important that during a replay of an event log, all these sagas are unwired, because you don't want new events to be fired during the replay!