Redux: reducing boilerplate

There’s no doubt Redux is pretty verbose. Don’t get me wrong, I know there is a good reason for that, but the price is still quite high. Fortunately, there is a way to reduce boilerplate without losing any fundamental parts of Redux.

I’m going to focus on one of these parts – reducers.

Let’s examine a very common implementation of a reducer which you can find on every article on the internet.

function todoApp(state = initialState, action) {
	switch (action.type) {
		case SET_VISIBILITY_FILTER:
			return {
				...state,
				visibilityFilter: action.visibilityFilter
			}
		case ADD_TODO:
			return {
				...state,
				todos: [
					...state.todos,
					{
						text: action.text,
						completed: false
					}
				]
			}
		default:
			return state
	}
}

Cool, but where is the single responsibility principle? A reducer is just a function, but this function knows too much. In this case our reducer implements a logic for adding a new todo item and handling filtering.

Also, official documentation of Redux pointed it out, so keep in mind general good practices in order to write a clean code.

Writing functions with single level of abstraction and single responsibility principle is much better practice. A function should do only one thing and every statement of a function should be on the same level of abstraction. By obeying these two laws (that are valid in object oriented and functional paradigm too, by the way) we can achieve cleaner and more readable functions.

Reducer is responsible for creating a new state based on the current state and the data that are coming through as an action payload.

As I mentioned before, you can find the way to solve this problem in Redux documentation. Extracting that low-level logic from every case statement into a separated handler function and creating a createReducer factory function with mapping between action types and handlers is way better.

But don’t forget that reducers are pure functions, so we can’t deal with the side effects here. The right place for the side effects is a middleware and therefore we have to split our logic between the middleware and the handler functions. Sometimes, it would not be so easy to decide which logic we should put in the middleware and which one belongs to the handler functions.

I’m going to show you slightly different approach.

First of all, let’s get rid of the low-level logic from our reducer as well!

In my opinion there exists even more straightforward way: we can simply put the whole logic into the middleware.   Now, reducers can only receive actions with final data which we are about to store in the state. We can simply merge current state and incoming data to create a new state.

Let’s make our reducer look like this.

function todoApp(state = initialState, action) {
	switch (action.type) {
		case SET_VISIBILITY_FILTER:
			return {
				...state,
				visibilityFilter: action.visibilityFilter
			}
		case ADD_TODO:
			return {
				...state,
				todos: action.todos
			}

		default:
			return state
	}
}

The Todo reducer is now much cleaner. But sooner or later we come across another issue. In every case in the switch statement we are forced to do the same thing. We just take a payload of an incoming action and merge it with the current state over and over again. Actually, I don’t like the switch statement at all. I believe we have (in OOP and also in Functional paradigm) more advanced way to solve this kind of situations.

We can create higher order function which serves us as a factory function for creating reducers. Similar to a createReducer from Redux documentation.

We have no longer to take care of reducers. The only thing we have to do is just call the factory function in order to get a particular reducer which will handle given actions for us.

const createReducer = (actionTypes, initialState) => (state = initialState, action) => {
	if(actionTypes.some(actionType => actionType === action.type)) {
		const { type, ...actionData } = action;
		return {
			...state,
			...actionData
		};
	}
	return state;
}

Now, we can create a todoApp reducer by calling a createReducer function.

const todoApp = createReducer([SET_VISIBILITY_FILTER, ADD_TODO], initialState);

That factory function accepts two parameters: an Array of action types and an initial state,  and returns a new function – reducer. Based on the given actionTypes array, we can easily find out if the incoming action belongs to this reducer and if so, we take all fields (excluding the type field) of the action and merge them with the current state. Otherwise, we just return the current state or the initial state.

Important thing is, that the fields of the action have to be named as the fields of the state which we are about to change.

Summary

We don’t need to take care of reducers anymore. A single line of code to create one. No big deal. Less code means less work and less room for mistakes. The logic is centralized in a middleware and actions carry final data for reducers. Reducers do one thing and know nothing about an implementation in a middleware.

I would love to know what you think about it. Don’t hesitate to give me some feedback.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s