Refactoring Reducer Logic Using Functional Decomposition and Reducer Composition
It may be helpful to see examples of what the different types of sub-reducer functions look like and how they fit together. Let's look at a demonstration of how a large single reducer function can be refactored into a composition of several smaller functions.
Note: this example is deliberately written in a verbose style in order to illustrate the concepts and the process of refactoring, rather than perfectly concise code.
Initial Reducer
Let's say that our initial reducer looks like this:
That function is fairly short, but already becoming overly complex. We're dealing with two different areas of concern (filtering vs managing our list of todos), the nesting is making the update logic harder to read, and it's not exactly clear what's going on everywhere.
Extracting Utility Functions
A good first step might be to break out a utility function to return a new object with updated fields. There's also a repeated pattern with trying to update a specific item in an array that we could extract to a function:
That reduced the duplication and made things a bit easier to read.
Extracting Case Reducers
Next, we can split each specific case into its own function:
Now it's very clear what's happening in each case. We can also start to see some patterns emerging.
Separating Data Handling by Domain
Our app reducer is still aware of all the different cases for our application. Let's try splitting things up so that the filter logic and the todo logic are separated:
Notice that because the two "slice of state" reducers are now getting only their own part of the whole state as arguments, they no longer need to return complex nested state objects, and are now simpler as a result.
Reducing Boilerplate
We're almost done. Since many people don't like switch statements, it's very common to use a function that creates a lookup table of action types to case functions. We'll use the createReducer
function described in Reducing Boilerplate:
Combining Reducers by Slice
As our last step, we can now use Redux's built-in combineReducers
utility to handle the "slice-of-state" logic for our top-level app reducer. Here's the final result:
We now have examples of several kinds of split-up reducer functions: helper utilities like updateObject
and createReducer
, handlers for specific cases like setVisibilityFilter
and addTodo
, and slice-of-state handlers like visibilityReducer
and todosReducer
. We also can see that appReducer
is an example of a "root reducer".
Although the final result in this example is noticeably longer than the original version, this is primarily due to the extraction of the utility functions, the addition of comments, and some deliberate verbosity for the sake of clarity, such as separate return statements. Looking at each function individually, the amount of responsibility is now smaller, and the intent is hopefully clearer. Also, in a real application, these functions would probably then be split into separate files such as reducerUtilities.js
, visibilityReducer.js
, todosReducer.js
, and rootReducer.js
.