Today I learned

Managing side effects in Redux

Redux is the preferred state management pattern for React apps today. Being a very "functional" library, it doesn't like side effects much. This means doing asynchronous actions in a Redux reducer is not just a bad idea, it simply won't work.

/* ✗ Warning: This won't work! */

function reducer (state, action) {
  switch (action.type) {
    case 'profile:load':
      fetch('/my_profile')
        .then(res => res.json())
        .then(res => {
          dispatch({            type: 'profile:set',            payload: res.body          })          // ✗ Error: dispatch is not defined
        })

        ...

You can't modify state here in an async callback. In fact, you can't even dispatch() in a reducer! This won't work and is a bad idea; you break the purity of the store reducer.

Using Middleware

Middleware for side effects

Fortunately, Redux has built-in provisions for managing side effects: Middleware! You can write your own middleware with business logic. You don't need to use 3rd-party packages other than Redux itself.

redux-middleware.js
function ProfileLoader () {
  return store => dispatch => action {
    // First pass them through to the reducers.
    dispatch(action)

    switch (action.type) {
      case 'profile:load!':
        fetch('/my_profile')
          .then(res => res.json())          .then(res => dispatch({ type: 'profile:set', payload: res.body }))          .catch(err => dispatch({ type: 'profile:error', payload: err }))
    }
  }
}

// Use the middleware in your store
store = createStore(reducers, {}, applyMiddleware(
  ProfileLoader()
)

Redux middleware is simply a decorator for dispatch(). Here's an example where we extend dispatch() to perform certain side effects (an AJAX call, in this case) when certain actions come in.

Other solutions

Using redux-thunk

Perhaps the most well-known solution to this is redux-thunk, which allows you to dispatch functions ("thunks").

// Using a function as an action via redux-thunk
store.dispatch((dispatch) => {
  fetch('/my_profile')
    .then((res) => res.json())
    .then((res) => dispatch({ type: 'profile:set', payload: res.body }))    .catch((err) => dispatch({ type: 'profile:error', payload: err }))})

However, I personally advise against this approach for a number of reasons:

  1. It moves logic to your action creators, which were supposed to be very simple pieces of code.

  2. It makes actions complicated, when they can just be simple JSON instructions (eg, { type: 'profile:load' }).

  3. It can't interact with other side effects. For instance, you can't make a side effect to send profile:errors to an error tracking service. Middleware can do this.

Naming convention

You may have noticed I named my action profile:load!. This is my preferred convention of choice, where action names are simply strings, and "side effect" actions are suffixed with an exclamation mark, just as it would in Ruby or Elixir.

Other examples

Error tracker

How about a middleware that tracks errors as they come in?

error-tracker-middleware.js
function ErrorTracker () {
  return store => dispatch => action {
    dispatch(action)

    switch (action.type) {
      case 'profile:error':
      case 'account:error':
      case 'other:error':
        Rollbar.track(action.payload)
    }
  }
}

Ticker

Or a middleware that sends tick events every second? Great for timers or for RPG's.

ticker-middleware.js
function Ticker (options) {
  let timer

  return store => dispatch => action {
    dispatch(action)

    switch (action.type) {
      case 'ticker:start!':
        timer = setInterval(() => {
          store.dispatch({ type: 'ticker:tick', now: new Date() })
        }, options.interval)
        break

      case 'ticker:stop!':
        if (timer) clearInterval(timer)
        timer = null
        break
    }
  }
}

Combining them

And to put them all together:

store = createStore(reducers, {}, applyMiddleware(
  ProfileLoader(),
  ErrorTracker(),
  Ticker({ interval: 1000 })
)

See also

Check out Redux on devguides.io! Devguides.io is my new pet project where I make pocket-sized explainers for web development concepts.

You have just read Managing side effects in Redux, written on March 14, 2017. This is Today I Learned, a collection of random tidbits I've learned through my day-to-day web development work. I'm Rico Sta. Cruz, @rstacruz on GitHub (and Twitter!).

← More articles