State as a Database

Learn how to resolve the issues in the previous lesson by treating the state as a database.

A recommended approach to solving the various issues raised in the previous lessons is to treat the application state as a database of entities. Like in a regular table-based database, we will have a “table” for each entity type with a “row” for each entity. The entity id will be our “primary key” for each table.

In our example, we will break down nesting to make our state as shallow as possible and express connections using IDs:

const state = {
  books: {
    21: {
      id: 21,
      name: 'Breakfast',
      recipes: [63, 78, 221]
    }
  },

  recipes: {
    63: {
      id: 63,
      book: 21,
      name: 'Omelette',
      favorite: true,
      preparation: 'How to prepare...',
      ingredients: [152, 121]
    },
    78: {},
    221: {}
  },

  ingredients: {}
};

In this structure, each object has its own key right in the root of our state. We can express any connections between objects (e.g., ingredients used in a recipe) using a regular ordered array of IDs.

Note: Normalizing refers to the idea of every data object having its own table with its stored IDs. The IDs are then used to link different data objects.

Let’s examine the implementation of the reducers needed to handle the ADD_INGREDIENT action using the new state structure:

const booksReducer = (books, action) => {
  // Not related to ingredients any more
};

// Recipe reducer now adds a recipe and links the ingredients using id.
const recipeReducer = (recipe, action) => {
  switch (action.type) {
    case ADD_INGREDIENT:
      return Object.assign({}, recipe, {
        ingredients: [...recipe.ingredients, action.payload.id]
      })
  }

  return recipe;
};

// Recipes reducer maps every recipe and runs the recipe reducer on each object.
const recipesReducer = (recipes, action) => {
  switch (action.type) {

    case ADD_INGREDIENT:
      return recipes.map(recipe =>
        recipe.id !== action.payload.recipeId
          ? recipe
          : recipeReducer(recipe, action));
  }
};
// Ingredients reducer adds ingredients
const ingredientsReducer = (ingredients, action) => {
  switch (action.type) {
    case ADD_INGREDIENT:
      return [...ingredients, action.payload];
  }
};

There are two things to note in this implementation compared to what we saw with the denormalized state:

  1. The booksreducer is not mentioned. Nesting levels only affect the parent and children, never the grandparents.
  2. The recipesreducer only adds an ID to the array of ingredients, not the whole ingredient object.

To take this example further, the implementation of UPDATE_RECIPE would not even require any change to the recipes reducer, as it can be handled fully by the ingredients reducer.

Get hands-on with 1200+ tech skills courses.