A Closer Look at Reducers

Learn about the reducers in depth.

If you open the reducers/root.js file, you will find that the same reducer is now taking care of different parts of our state tree. More properties will be added to both the recipes and the ingredients subtrees as our application grows. Since the code in both handlers is not interdependent, we can split it further into three reducers, two that are responsible for different parts of the state and one to combine them:

const recipesReducer = (recipes, action) => {
  switch (action.type) {
    case 'ADD_RECIPE':
      return recipes.concat({name: action.name});
  }

  return recipes;
};

const ingredientsReducer = (ingredients, action) => { ... }

const rootReducer = (state, action) => {
  return Object.assign({}, state, {
    recipes: recipesReducer(state.recipes, action),
    ingredients: ingredientsReducer(state.ingredients, action)
  });
};

Since we are using multiple reducers, let’s extract them into their own files to follow the rule of one reducer per file. We can import the subreducers into the root reducer to be used by the store:

Add this to the reducers/recipes.js file:

const recipesReducer = (recipes = [], action) => {
  switch (action.type) {
    case 'ADD_RECIPE':
      return recipes.concat({ name: action.name });
  }

  return recipes;
};

export default recipesReducer;

Add this to the reducers/ingredients.js file:

const ingredientsReducer = (ingredients = [], action) => {
  switch (action.type) {
    case 'ADD_INGREDIENT':
      const newIngredient = {
        name: action.name,
        recipe: action.recipe,
        quantity: action.quantity
      };

      return ingredients.concat(newIngredient);
  }

  return ingredients;
};

export default ingredientsReducer;

Add this to the reducers/root.js file:

import recipesReducer from './recipes';
import ingredientsReducer from './ingredients';

const rootReducer = (state, action) => {
  return Object.assign({}, state, {
    recipes: recipesReducer(state.recipes, action),
    ingredients: ingredientsReducer(state.ingredients, action)
  });
};

export default rootReducer;

There are three main benefits to extracting subtree management functionality into separate reducers:

  1. The root reducer only creates a new state object by combining the old state and the results of each of the subreducers.
  2. The recipes reducer is much simpler as it only has to handle the recipes part of the state.
  3. All the other reducers don’t need to know about the internal structure of the recipes subtree. Thus, changes to that part of the state tree will only require changes to the recipe reducer.

Additionally, we can tell each reducer how to initialize its own subtree instead of relying on a big initialState passed as an argument to the createStore() function.

To initialize state for recipes reducer, add the following:

const initialState = [];

const recipesReducer = (recipes = initialState, action) => { ... };

We can similarly implement for other subusers and then remove the initialState definition and argument from store.js:

import { createStore } from 'redux';
import rootReducer from './reducers/root';

const store = createStore(rootReducer);

window.store = store;

export default store;

Since combining multiple reducers is a pervasive pattern, Redux has a special utility function, combineReducers(), which performs the role of our root reducer. Let’s replace our implementation in reducers/root.js using this combineReducers() function:

import { combineReducers } from 'redux';
import recipesReducer from './recipes';
import ingredientsReducer from './ingredients';

const rootReducer = combineReducers({
  recipes: recipesReducer,
  ingredients: ingredientsReducer
});

export default rootReducer;

Get hands-on with 1200+ tech skills courses.