Structuring Redux Reducers by Action Type

I recently refactored a personal project of mine written using React & Redux. I've been thinking a lot recently about how to organize reducers with Redux. The Redux documentation has a lot of good material worth checking out on this topic, particularly this article on what they term "case functions," which basically means having a dedicated function to handle the logic of how the state transforms for a given action type.

Previously I utilized the reduce-reducers library, and organized my reducers both temporally and structurally: I wanted each reducer to be responsible for transforming only one section of the state, but for each be able to reference other sections of state as necessary (a structural isolation, but not as strict as using combineReducers); and I arranged for them each to transform the state in a strict ordering via reduce-reducers (the temporal isolation). This worked pretty well for the first iteration of my app, where I effectively had two sections of my state (call them stateA and stateB) and two corresponding reducers, and I knew that reducerA didn't need to reference anything in stateB. Using reduce-reducers, whenever there's an action, first reducerA transforms the state, then reducerB.

The spanner in the works came when I wanted to add additional behavior to app which required reducerA to reference stateB. Since stateB isn't updated until after reducerA completes its transformation, any references in reducerA to stateB are effectively "stale"—that section of state is due to be transformed, but hasn't been yet.

So, out with reduce-reducers, in with case functions, which is a different design pattern that organizes your reducers by action type. With this pattern you'll generally have a "core" reducer with a pretty simple logic: figure out the type of the action, and call the corresponding case function. What might this look like in practice?

import * as deleteEntity from './deleteEntity';
import * as newEntity from './newEntity';
import * as nextEntity from './nextEntity';
import * as updateEntity from "./updateEntity";

const initialState = {
  order: {
    active: null,
    ids: []
  },
  entities: {}
};

const caseFunctions = {
  [updateEntity.type]: updateEntity.reducer,
  [newEntity.type]: newEntity.reducer,
  [deleteEntity.type]: deleteEntity.reducer,
  [nextEntity.type]: nextEntity.reducer
};

function coreReducer(state = initialState, action) {
  const reducer = caseFunctions[action.type];
  if (reducer != null) {
    return reducer(state, action);
  }

  return state;
}

export default coreReducer;

This is all neat, but the light-bulb moment in my head came when I realized I can organize an individual case function any way I can organize a reducer generally—including temporally. Consider this code for newEntity and nextEntity.

// === newEntity.js ==========================
import { NEW_ENTITY } from "../actions/types";
import flow from "lodash/fp/flow";

const createTheEntity = ({ newEntityId }) => state => {
  const newState = /* logic for adding the new entity */
  return newState;
};

const updateTheEntityOrdering = ({ newEntityId }) => state => {
  const newState = /* logic for updating the entity order */
  return newState;
};

export function reducer(state, action) {
  const newEntityId = action.payload;
  return flow(
    createTheEntity({ newEntityId }),
    updateTheEntityOrdering({ newEntityId })
  )(state);
}

export const type = NEW_ENTITY;

// === nextEntity.js ==========================
import { NEXT_ENTITY } from "../actions/types";
import flow from "lodash/fp/flow";

const progressEntityOrderingToNextEntity = state => {
  const newState = /* logic for progressing the active entity */
  return newState;
};

const updateNewlyActiveEntity = state => {
  const newState = /* logic to update the newly active entity */
  return newState;
};

export function reducer(state, action) {
  /*
   * The NEXT_ENTITY action doesn't have a payload, so
   * we don't need to curry the transformers.
   */
  return flow(
    progressEntityOrderingToNextEntity,
    updateNewlyActiveEntity
  )(state);
}

export const type = NEXT_ENTITY;

Essentially, each of my case functions consists of a core reducer function and two or more of what I call "transformers." Transformers are functions of the following signatures.

// generally
(...args) => prevState => newState

// or, when the transformer is a pure function of prior state
prevState => newState

The former signature is utilized in newEntity and the latter is demonstrated in nextEntity. The benefit of adopting this structure is that it allows me to order the transformers by composing them using lodash's flow higher-order function. (Any right-compose function would work.) Most of these case functions mirror the original ordering back from when I was using reduce-reducers: the first transformer updates stateA, and then the second transforms stateB. But the benefit to using case functions here is that I can reverse this ordering when I need to, as with nextEntity.

In brief, I recommend organizing your redux reducers by type using case functions, and then then mitigating complexity within case functions through function composition.