Skip to content

Aquent | DEV6

Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages

Use Object maps to tidy up your Redux reducers

Written by: Rod Nolan

I write React apps and I like Redux for state management when I’m working on non-trivial apps with a lot of data to manage or with a component tree that’s more than a few levels deep.

In this post, I want to share a small tip you can use to de-clutter your reducer functions. I assume that you are already familiar with Redux basics. Terms like action and reducer should not be new to you but if they are, here are the basic facts:

1. Actions are just custom events. They are implemented as plain objects with a type property, a unique string that allows your reducers to differentiate one from another, and an optional payload property, which holds the data required to mutate state.

2. Reducers are pure functions whose job is to produce a new state based on two arguments

  • the existing state, and
  • an action that describes how to mutate that state.

So, the signature of a reducer function is simply:

(state, action) => state

3. Redux calls your reducer functions automatically whenever your app dispatches ANY action, even the ones your reducer doesn’t handle. If a reducer doesn’t handle an action it must return the state it received, unchanged.  

To put all this in context, let’s examine a simple example:

myReducer.js

const defaultState = {
	prop1: ''
}
export const myReducer = (state = defaultState, action) => {
	// calculate new state, based on the action received, and return it
	// if the action is not related to prop1, just return existing state
}

A common pattern for building reducers is to use a switch/case statement to separate the code blocks for each of the actions the reducer expects. For example, if you’re managing a piece of state called counter, the reducer might handle different actions for incrementing, decrementing and resetting that value.

counterReducer.js

const defaultState = {
	counter: 0
}

export const counterReducer = (state = defaultState, action) => {
	switch (action.type) {

		// calculate new state, based on the action received, and return it.
		case 'increment':
			return state.counter + 1;
		break;
		case 'decrement':
			return state.counter - 1;
		break;
		case 'reset':
			return 0;
		break;

		// if the action is not related to counter, just return counter
		default: 
			return state; 
		break;
	}
}

When state is simple or when there are only a few actions to deal with, switch/case works well. It’s easy to see at a glance how the state will change in response to each action. But a simple 10- or 15-line reducer function can quickly grow to an unmanageable monster as the number of actions or the complexity of the state grows. To battle this bloat, I like to use object maps. Here’s what the code looks like:

counterReducer.js

const defaultState = {
	counter: 0
}

const incrementHandler = (state, action) => state.counter + 1;
const decrementHandler = (state, action) => state.counter - 1;
const resetHandler = (state, action) => 0;
// implement other handlers for new action types to calculate/return state

export const counterReducer = (state = defaultState, action) => {
	const actionHandlers = {
		increment: incrementHandler,
		decrement: decrementHandler,
		reset: resetHandler
		// add new properties as required for new action types
	}

	const handler = actionHandlers[action.type];
	return handler ? handler(state, action) : state;
}

Things to note:

1. Handler functions are named in a way that properly describes what they do to the state, making your code easier to understand.

2. Handler functions are defined outside of the reducer, decreasing the overall complexity of that one function… goodbye code bloat.

3. The properties of the actionHandlers object hold references to all the functions that process actions of interest to this reducer. For each property:

  • name matches the action.type property
  • value is a reference to the associated handler

4. If the action type is not of interest in this reducer, the handler variable will be null so the function will return the state that it received, unchanged. This is the equivalent of the default block in a switch/case statement.

While the handler functions defined above are admittedly very simple, they now have room to grow without cluttering up the traffic cop function that is the main reducer.

You usually encounter this code clutter when your state is more complex than a simple primitive value (an array of objects, for example). In this case you might need to handle actions for adding and deleting array items and for updating individual properties in the objects. The deeper the data structure, the more steps are required to calculate an update and it’s in those situations where this object maps pattern really shines because all of the mutation code for each operation is extracted out to its own function.

arrayOfObjectsReducer.js

defaultState = {
	items: [] // contains objects, not simple primitives
}

function updateObjectInArray (state, action) {
	const {index, propName, propValue} = action.payload;
	let newStateToReturn = {...state};
	let itemToUpdate = {...newStateToReturn.items[index]};
	itemToUpdate[propName] = propValue;

	newStateToReturn.items[index] = itemToUpdate;
	return newStateToReturn;
}
// more state mutation functions...


export const arrayOfObjectsReducer = (state = defaultState, action) => {
	const actionHandlers = {
		updateItem: updateObjectInArray,
		// more props for other state mutation functions...
	}

There’s one other thing you can (and should) do to decrease the likelihood of runtime bugs creeping in: define those strings you use for the type property in all your action objects as constants.

actionTypes.js

export const UPDATE_ITEM = 'UPDATE_ITEM';

actions.js

import {UPDATE_ITEM} from './actionTypes';
 
export const createUpdateAction = (p1, p2) => ({
	type: UPDATE_ITEM,
	payload: {
		p1, p2
	}
})

arrayOfObjectsReducer.js

import {UPDATE_ITEM} from './actionTypes';

...

export const arrayOfObjectsReducer = (state = defaultState, action) => {
	const actionHandlers = {
		updateItem: updateObjectInArray,
		[UPDATE_ITEM]: updateObjectInArray
		// more props for other state mutation functions...
	}

	const handler = actionHandlers[action.type];
	return handler ? handler(state, action) : state;
}

By replacing string literals with variables, the code completion features in any modern IDE combined with a code linter will help you to avoid those hard-to-detect runtime errors caused by typos. OK, so that’s two tips instead of one. In any case, this solution is simple and elegant, and it scales nicely as your list of action handlers grows.

If you’re trying to level up your web dev skills, check out our 3-day React course here: ReactJS