Managing Complexity in Redux: Higher-Order Reducers and Async State

This post follows the story of an app which has been built with Redux but has somewhere, somehow taken a turn for the unmaintainable – particularly in regards to how it handles async loading state. It then outlines a few general Redux patterns which can be used to help steer the app in a more maintainable direction.

Note: The post will not be about sagas or epics – which are concerned with managing async state, but also generally with managing effects – but more about useful Redux patterns told through the example of async state management.

So we want to build an app?

Good idea! What else are we going to do with these weird heaps of silicon and plastic anyway. And we’ll build it with the Flux-inspired Redux? Very elegant and maintainable! At least, we hope so. All the examples online definitely look that way. Yet, as we start to build, we quickly run into problems which seem to fall outside the scope of those simple examples and we begin to feel more and more unsure of how best to move forward. Worse still, in the midst of it all, we find our application actually becoming increasingly more inelegant and unmaintainable. Even simple changes seem to require a slew of actions, reducers, selectors, and components. What’s the deal? Was this Redux thing all buzz and hype?

Let’s take a look at our app and see if we can’t find what went wrong, specifically in one area which tends to be a non-trivial source of complexity: network requests and async state.

How did we even get here

You don’t remember? Well, we started with a simple and noble goal: to build an app to help people inventory the food in their kitchens. It began with an action creator:

const addFoodItem = item => ({ type: 'ADD_FOOD_ITEM', payload: item })  

Which was dispatched by a button click in our UI and then reduced into our application state:

const reducer = (state, { type, payload }) => {  
  switch (type) {
    case 'ADD_FOOD_ITEM':
      return {
        foodItems: [
          ...state.foodItems,
          payload
        ]
      }
    default:
      return state;
  }
}

Which in turn was rendered (we're using React):

const App = ({ foodItems, addFoodItem }) => (  
  <div>
    <ul>
      {
        foodItems.map(({ name, quantity }) => (
          <li>{name}, quantity: {quantity}</li>
        ))
      }
    </ul>

    <button onClick={() => addFoodItem({ name: '🍋', quantity: 50 })}>
      Add Food Item
    </button>
  </div>
)

Well, this is where we were after reading a few tutorials. Of course, it was still a good ways away from having a completed app. We hadn’t even begun accepting user input!

(And in fact, we won’t cover the topic of form handling in this post – it being another common source of complexity in Redux apps – other than to point and grunt in the general direction of Redux Form)

OK and so where are we exactly?

Indeed, in the name of UX we needed our app to work seamlessly across all of our users’ devices. For this we wanted a server to store their data. After writing our server code and hooking up our application using Redux Thunk, things ended up looking like this:

const FOOD_ITEMS_FETCH_PENDING = 'FOOD_ITEMS_FETCH_PENDING'  
const FOOD_ITEMS_FETCH_COMPLETE = 'FOOD_ITEMS_FETCH_COMPLETE'  
const FOOD_ITEMS_FETCH_ERROR = 'FOOD_ITEMS_FETCH_ERROR'  
const FOOD_ITEMS_ADD_PENDING = 'FOOD_ITEMS_ADD_PENDING'  
const FOOD_ITEMS_ADD_COMPLETE = 'FOOD_ITEMS_ADD_COMPLETE'  
const FOOD_ITEMS_ADD_ERROR = 'FOOD_ITEMS_ADD_ERROR'

// …

const addFoodItem = item => (dispatch) => {  
  dispatch({ type: FOOD_ITEMS_ADD_PENDING })

  foodService
    .addFoodItem(item)
    .then(payload => dispatch({ type: FOOD_ITEMS_ADD_COMPLETE, payload }))
    .catch(payload => dispatch({ type: FOOD_ITEMS_ADD_ERROR, payload }))
}

const fetchFoodItems = item => (dispatch) => {  
  dispatch({ type: FOOD_ITEMS_FETCH_PENDING })

  foodService
    .fetchFoodItems(item)
    .then(payload => dispatch({ type: FOOD_ITEMS_FETCH_COMPLETE, payload }))
    .catch(payload => dispatch({ type: FOOD_ITEMS_FETCH_ERROR, payload }))
}

// …

const reducer = (state, { type, payload }) => {  
  switch (type) {
    case FOOD_ITEMS_FETCH_PENDING:
      return {
        ...state,
        foodItemsLoaded: false
      }
    case FOOD_ITEMS_FETCH_ERROR:
      return {
         ...state,
         foodItemsLoaded: false,
         foodItemsError: payload
      }
    case FOOD_ITEMS_FETCH_COMPLETE:
      return {
        ...state,
        foodItemsLoaded: true,
        foodItems: payload
      }
    case FOOD_ITEMS_ADD_PENDING:
      return {
        ...state,
        foodItemsAdding: true
      }
    case FOOD_ITEMS_ADD_ERROR:
      return {
        ...state,
        foodItemsAdding: false
      }
    case FOOD_ITEMS_ADD_COMPLETE:
      return {
        ...state,
        foodItemsAdding: false,
        foodItems: [
          ...state.foodItems,
          payload
        ]
      }
    default:
      return state;
  }
}

// …

const App = ({  
  foodItems,
  foodItemsLoaded,
  foodItemsAdding,
  addFoodItem
}) => (
  foodItemsLoaded
    ? (
      <div>
        <ul>
          {
            foodItems.map(({ name, quantity }) => (
              <li>{name}, quantity: {quantity}</li>
            ))
          }
        </ul>

        <button
          onClick={() => addFoodItem({ name: '🍋', quantity: 50 })}
          disabled={foodItemsAdding}
        >
          Add Food Item {foodItemsAdding ? <Spinner /> : ''}
        </button>
      </div>
    )
    : <Spinner />
)

That's a lot of boilerplate. And we haven't even got to any cool features yet 😭 Sure, we were able to get things done using a tried and true method, but we can already feel the red hot pain on the horizon. It’s obvious that something somewhere here can be abstracted, but how exactly? This whole pattern in Redux of reducers on global actions seems to steer us directly towards duplication of both code and logic (and away from composability). Introduce a new component? Well it probably needs its own state slice, and thus its own reducer. And clearly it needs its own actions somehow: we don't want this new component to react to changes on an old one.

How do we get out of here

If it’s not broke, don’t fix it! Unfortunately for us, broke is in the eye of the beholder and our beholders want more features. They don’t want just a food inventory, but an entire kitchen assistant! They want to inventory their cookware as well as store and discover recipes.

We start by thinking about the cookware inventory. It seems to have pretty much the same requirements as our food inventory. We’re fairly good at abstracting components and can immediately think of ways to generalize our list component to handle different list types. However, when it comes to our async state logic things are more unclear.

If we were to follow the previous pattern, we would now need to introduce three new action types: one each for the pending, error, and completed states of our new data. We would need another (relatively heavy) action creator to orchestrate their dispatch as well as reducer logic for all three types. We can continue to create our action types one by one (as seems to be recommended by the Redux docs), but we definitely want to find a way to reuse our action creator and reducer.

What we need to do (and this technique can be found in the previous doc link, as well as here and here) is write higher-order action creators and reducers, i.e. action creator and reducer creators.

Creating a reusable action creator

How do we create actions? Well, with functions which produce actions (a.k.a. action creators). And so how can we create action creators? Well, with functions which produce functions which produce actions! (Or which produce thunks in our case)

The following is a very simple example of a higher-order action creator for our – as we’ll call them – async actions:

// Here we have a function which accepts a map of async states to action types
// as well as a thunk creator. It will yield a new thunk creator which wraps
// the provided one with our async state logic
const asyncActionCreator = (asyncTypes, createThunk) => (...args) => {  
    const thunk = createThunk(...args);

    return (dispatch) => {
      dispatch({ type: asyncTypes.pending })

      // We assume here that the wrapped thunk produces a Promise
      // We call dispatch on the thunk (it's just a normal thunk, after all)
      // and since dispatch yields its result, we can utilize the returned
      // Promise
      return dispatch(thunk)
        .then(payload => ({
          type: asyncTypes.complete,
          payload
        }))
        .catch(err => ({
          type: asyncTypes.error,
          error: true,
          payload: error
        }))
    }
}

Now whenever we need an action creator for our async actions, we simply do the following:

const fetchFoodItems = createActionCreator(  
  {
    pending: FOOD_ITEMS_FETCH_PENDING,
    complete: FOOD_ITEMS_FETCH_COMPLETE,
    error: FOOD_ITEMS_FETCH_ERROR,
  },
  () => () => foodService.fetchFoodItems()
)

const addFoodItem = createActionCreator(  
  {
    pending: FOOD_ITEMS_ADD_PENDING,
    complete: FOOD_ITEMS_ADD_COMPLETE,
    error: FOOD_ITEMS_ADD_ERROR,
  },
  // Here's an example where we take advantage of the fact we are wrapping
  // totally normal thunk creators by also dispatching within the wrapped thunk
  item => dispatch => (
    foodService
      .addFoodItem(item)
      .then((result) => {
        dispatch(recordAnalytics({ event: 'OMG_SOMEONES_USING_THE_APP' }))
        return result
      })
  )
)

Easy!

Note: We also have other options here. Instead of creating multiple action types, we could attach additional information to actions of a single type. action.name, for example, could be used by a reducer over FOOD_ITEMS_FETCH to differentiate async state. Furthermore, we could use middleware in place of the higher-order action creator (scroll to the callAPIMiddleware definition) to orchestrate async state dispatches. All viable options!

Another note: this particular pattern assumes a 1 to 1 correspondence between API calls and resources. If you’re using normalizr this won’t necessarily be the case. One (potentially naive) solution might be to write action creators which simply dispatch more actions: instead of 3 for a single resource, 3 for each resource in the query. In the case of Falcor or GraphQL, it would probably be simplest to track view loading state rather than resource loading state.

Creating a resusable reducer

When it comes to implementing our reducers, the principle is the same: we create higher-order functions which produce reducers. Here is a simple implementation for reducing the async actions we defined earlier into loading state:

// Similar to the higher-order action creator, we accept a mapping of async
// states and action types, except this time, for convenience, in reverse.
// We then yield a reducer
const loadStateReducer = (asyncStates) => {  
  return (state = { loading: false, loaded: false, data: null }, action) {
    const asyncState = asyncStates[action.type]

    switch (asyncState) {
      case 'pending':
        return {
          loading: true,
          loaded: false,
          data: state.data
        }
      case 'error':
        return {
          loading: false,
          loaded: false,
          error: action.payload,
          data: state.data
        }
      case 'complete':
        return {
          loading: false,
          loaded: true,
          data: action.payload
        }
      default:
        return state
    }
  }
}

Not so bad! We've even extended the amount of load state information we’re storing, which will allow us to write richer UIs. We can use our reducer creator like this:

const { combineReducers } = require('redux')

const foodItemsLoadState = loadStateReducer({  
  FOOD_ITEMS_FETCH_PENDING: 'pending',
  FOOD_ITEMS_FETCH_ERROR: 'error',
  FOOD_ITEMS_FETCH_COMPLETE: 'complete'
})

const foodItemAddState = loadStateReducer({  
  FOOD_ITEMS_ADD_PENDING: 'pending',
  FOOD_ITEMS_ADD_ERROR: 'error',
  FOOD_ITEMS_ADD_COMPLETE: 'complete'
})

const foodItemAdd = (state, { type, payload }) => {  
  switch (type) {
    case FOOD_ITEMS_ADD_COMPLETE:
      return { ...state, data: [ ...state.data, payload ] }
    default:
      return state
  }
}

const foodItems = (state, action) => (  
  foodItemAdd(foodItemsLoadState(state, action), action)
)

const reducer = combineReducers({  
  foodItems,
  foodItemAddState
})

With this, suddenly, we have achieved something even more fundamental than code reusability: we’ve created a data abstraction! We now have a de-facto type, let’s call it “async data”, which we know can exist in one of 4 states: uninitiated, pending, errored, and completed. We can write totally generic functions to process this data, we can reuse these functions throughout our app, we can keep this logic separate from our domain / business logic, and – very crucially – we can be in agreement with a nice aphorism from a cool computer science person:

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.

All in all, it’s looking good. We go home and sleep peacefully.

And for the suspicious: these reducers might smell a bit imperfect! Why, for example, does the foodItemAddState reducer store a data payload, when all we want from it is the loading state? And why does foodItemAdd need to know about our async data shape when all it should care about is the application data? True! The important thing, though, is that we have moved from a less to a more composable reducer pattern. If we later decide it was a bad idea to nest data payloads under their loading states and want to give them their own state slices, we’ll have an easier time doing that. (We could also, by the way, modify loadStateReducer to accept another reducer as parameter and use that to reduce just the data payload)

Reaping the benefits

Waking up from our peaceful sleep, we feel fresh and emboldened! Software will eat the world! And we’ll implement all the features! We see version 3.0 of our application clearly in front of our inner eye: it does virtually everything. (And soon it may produce enough heat to actually cook food)

Preparing for greatness

The loading states of our new app will, of course, be more complex than before. For example, what will we do when we have views which depend on multiple async data sources? (Currently, each list component only needs to monitor the loading state of one) And what about components which depend on aggregations or mappings of this async data?

Regarding the first issue (that is, the problem of waiting to render until after X number of dependencies have loaded), we think about letting components deal with aggregate loading state. They’ll receive multiple load state parameters and perform some boolean logic to figure out the total loading state. That doesn’t feel right, though. In the general case, our components just want to know whether they can render or whether they need to show a spinner.

Then we consider storing the aggregate loading state as state. Maybe in our action creators we fire multiple requests and only trigger completion when all finish successfully. We can then reduce those single actions to the loading states of sets of resources. But we don’t fall prey to this idea. We remember our quote from the cool computer science person. Indeed, why create more state? We want to have the absolute least amount of state possible. We have a better idea.

Writing selectors on async data

Instead, we realize that the load state of a set of resources is computable from the load states of each. In other words: it’s derived state. And we know how to deal with derived state in Redux: with so-called selectors.

We start by creating selectors for each of our async resources:

const getAsyncFoodItems = state => state.foodItems  
const getAsyncCookware = state => state.cookware  
// Let’s introduce a new async resource containing fun facts about food
const getAsyncFunFactsById = state => state.funFactsById  

Note: Check out Dan Abramov’s video on co-locating selectors with reducers for good tips on how you might set this up

We would now like to be able to compose these selectors, and treat the result as a single “slice” of async data. We would like to write something like this:

const getFoodItemListViewState = createAsyncSelector({  
  foodItems: getAsyncFoodItems,
  funFactsById: getAsyncFunFactsById
})

Or maybe something like this:

const getAsyncFoodItemsWithFacts = createAsyncSelector(  
  {
    foodItems: getAsyncFoodItems,
    funFactsById: getFunFactsById
  },
  ({ foodItems, funFactsById }) => (
    mergeFunFactsIntoFoodItems(foodItems, funFactsById)
  ))
)

Where the computed result conforms to the async data shape we established earlier. Then our component, which we have modified to depend on data from two remote sources, may look like this:

const FoodList = ({ loaded, data }) => {  
  if (!loaded) {
    return <Spinner />
  }

  const { foodItemsWithFacts } = data;

  return (
    <ul className="food-list">
      {
        foodItemsWithFacts.map(({ name, quantity fact}) => (
          <li>
            <p>{name} quantity: {quantity}</p>
            <p>{fact}</p>
          </li>
        ))
      }
    </ul>
  )
}

Here is one possible implementation:

(The example is utilizing reselect)

const { createSelector, createStructuredSelector } = require('reselect')  
const identity = x => x

const createAsyncSelector = (  
  asyncSelectors = {},
  syncSelectors = {},
  compute = identity
) => {
  // Let’s do a little argument dance to optionally support the uniform
  // handling of synchronous selectors (selectors on data not conforming
  // to our async data format) as well as allowing the user to specify
  // a state derivation function
  if (arguments.length === 2 && typeof syncSelectors === 'function') {
    compute = syncSelectors
    syncSelectors = {}
  }

  // Create selectors functions from the user supplied maps
  const getAsync = createStructuredSelector(asyncSelectors)
  const getSync = createStructuredSelector(syncSelectors)
  // We’ll store the async data key names to use later
  const asyncKeys = Object.keys(asyncSelectors)

  return createSelector(
    getAsync,
    getSync,
    (asyncData, syncData) => {
      // Let’s compute! What we want to do is iterate through all slices of
      // async data (these { loading, loaded, data } objects), and derive
      // their total loading state. Remember, we have 4 possible states:
      // uninitiated, loading, completed, and errored

      // We say that, if one async data slice is uninitiated,
      // all are uninitiated
      if (
        asyncKeys.some(key => (
          !asyncData[key].loading &&
          !asyncData[key].loaded &&
          !asyncData[key].error
        ))
      ) {
        return { loading: false, loaded: false, data: null }
      }

      // Likewise, if one is loading, they are all loading
      if (asyncKeys.some(key => asyncData[key].loading)) {
        return { loading: true, loaded: false, data: null }
      }

      // And the same for errors
      if (asyncKeys.some(key => asyncData[key].error)) {
        return {
          loading: false,
          loaded: false,
          error: true // TODO We should probably put a list of errors here
        }
      }

      // Otherwise we’re ready to run the selector!
      return {
        loading: false,
        loaded: true,
        data: compute({
          // Merge in the sync slices so that the compute function may also
          // take them into account
          ...syncData,
          // We also of course merge in the async slices. Here, however, we want
          // to transform them into just their `data` payloads so that the
          // compute function can operate on the data alone without worrying
          // about load state
          ...asyncKeys.reduce((acc, key) => {
            acc[key] = asyncData[key].data
            return acc
          })
        })
      }
    }
  )
}

Powerful stuff! We’ve solved both the problem of representing aggregate loading state as well as that of deriving state from async data, and we did it by turning the two problems into one. Efficient!

Note: there’s still one somewhat important detail missing in our implementation of createAsyncSelector: it doesn’t preserve the old data payload the same way our reducer does, which would happen, for example, when moving from a LOADED to LOADING state. But how do we get the old data payload if we’re inside a pure function? This can be implemented by creating a custom selector creator which wraps the default memoize function such that you can store a reference to the memoized result and use it in your data derivation function. Phew!

Conclusion

We’ve done it! We've written the most elegant application in the world. Starting from a bit of blog post copy-paste and iterating into some worrying complexity, we were successfully able to abstract our code in a more maintainable way, as well as identify a few general patterns we will be able to reuse in the future.

And now, our legacy will spin on into eternity: spinner.gif

blogfoster’s vision is to build an ecosystem for bloggers where they can get all the tools and support they need to become successful with their blogs. We use React, Redux, Webpack, SASS, ES6 and more to build an enjoyable platform for thousand of bloggers. Do you want to work with the newest technologies? We are constantly looking for people as passionate as we are. Join our team, let's work together.