I recently refactored a personal project of mine written using React & Redux. I've been thinking a lot 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 have a "core" reducer with a 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.