Normalizing the Data

Let’s have a look at how we can use the `normalizr` library to normalize the data.

With the reducers updated and the action structure defined, we are left to extract the payload from the denormalized data we got from the server.

A simple approach might have a custom function that knows each API’s shape and normalizes the returned nested JSON into a flat structure with custom code.

Since this is quite a common practice, the normalizr library can replace custom code. We can define the schema of the data coming from the server and have the normalizr code turn our nested JSON into a normalized structure we can pass directly into our new UPDATE_DATA action.

To use the library, we first define the schema for our data:

import { schema } from 'normalizr';

export const ingredient = new schema.Entity('ingredient');

export const recipe = new schema.Entity('recipe', {
  ingredients: [ingredient]
});

Here we defined two entity types, a basic ingredient and a recipe containing an array of ingredient objects.

The normalizr library allows for much more complex schemas. Consult the documentation for details.

Once we have obtained the data from the server and defined a schema for the data, we can use normalizr to normalize the data automatically:

import { normalize } from 'normalizr';

const data = normalize(data, recipe);

The data object will now hold two keys:

  1. entities: An object containing the normalized entities, arranged by entity type
  2. result: The ID of the main object or array (if we passed multiple recipes as data)

Inside the entities key, we will find the normalized data:

{
  ingredients: {
    5123: {
      id: 5123,
      name: 'Egg',
      quantity: 2
    }
  },
  recipes: {
    63: {
      id: 63,
      name: 'Omelette',
      favorite: true,
      preparation: 'How to prepare...',
      ingredients: [5123]
    }
  }
}

The normalizr library did all the hard work for us. Now we can update our store by sending the relevant parts to each reducer:

dispatch({ type: 'SET_INGREDIENTS', payload: data.entities.ingredients });
dispatch({ type: 'SET_RECIPES', payload: data.entities.recipes});

With this approach, each reducer only has to handle its own entity types, making the reducer code simple and nondependent on other actions and entities.

It is best to keep the schemas of each entity in a separate (and reusable) place in your application. E.g., in a lib/schema.js file.

Get hands-on with 1200+ tech skills courses.