The benefits of application state normalization in Angular

Vamsi Vempati
Angular In Depth
Published in
9 min readMay 1, 2018
Photo by Tony Webster on Unsplash

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

Imagine we have a recursive data structure in the store, let us say, information about a product’s category in an e-commerce application. Category is the classification of which type of product it is. For example, Mobile Phones category can have subcategories such as Google, Apple, Samsung and so on and each subcategory can in turn have further subcategories — Google subcategory can have further subcategories —Google Pixel, Google Pixel XL, Google Pixel 2 & Google Pixel 2 XL . These subcategories would be of the same type as their parent categories — ICategory. This is called recursive data type.

Response from the API for Mobile Phones

The information about the Mobile Phones category is available from the API and when we retrieve it, we save the response in the store. Let us say, we want to get information about Google Pixel category.

Instead of making another API call, we can check if we have the information is in the store already, if we do, we get it from there and if we do not have the required information in the store, we will make an API call to get it.

Why should we normalize the state?

Looking at the recursive data structure, the check to see if we already have the information is not quite straight forward. We can still do it but we would need some kind of looping mechanism or binary tree algorithm to loop through the different categories in the store and see if Google Pixel is in it.

We could instead flatten the category structure, map the categories against the category ids and save it in the store. This way, it is easier to look up if the data is already in the store. This process of flattening the data is called normalization. When we retrieve the information from the store, we would do the reverse of normalization, which would be to take the flattened data and revert it to a nested object. This is called denormalization.

Advantages:

  • Decreased memory usage of the app
  • Easier to work with the nested objects to retrieve a nested object and faster to access
  • No nasty code to look up values in the nested objects

Other reasons mentioned in redux.js docs:

  • Because each item is only defined in one place, we don’t have to try to make changes in multiple places if that item is updated.
  • The reducer logic doesn’t have to deal with deep levels of nesting, so it will probably be much simpler.
  • The logic for retrieving or updating a given item is now fairly simple and consistent. Given an item’s type and its ID, we can directly look it up in a couple simple steps, without having to dig through other objects to find it.
  • Since each data type is separated, an update like changing the text of a comment would only require new copies of the “comments > byId > comment” portion of the tree. This will generally mean fewer portions of the UI that need to update because their data has changed. In contrast, updating a comment in the original nested shape would have required updating the comment object, the parent post object, the array of all post objects, and likely have caused all of the Post components and Comment components in the UI to re-render themselves.

normalizr

normalizr by Paul Armstrong is a good library to use to achieve what we want here - it can convert deeply nested objects into a flattened structure and vice versa.

In an Angular @ngrx application, normalization is better done in the effects before we dispatch an action to update the store and denormalization in the selector before we provide the value from the store to its consumers.

The process would comprise of the following steps:

Normalization:

  • An action is fired to get the details about the category
  • The effect listens to the action and checks if the information is already present in the store by using the selectors for category
  • If the information is available in the store, we dispatch an action with the category details from the store returned by the selector and finish the effect
  • If the information is not available in the store, an API call would be made to retrieve the data and normalize it (take all descendant categories off the response and flatten them)
  • An action is fired from the effect to update the store and the effect is finished
  • Reducer updates the store with the information

Denormalization:

  • Request for information about a category happens by calling the selectors
  • Selector checks if the category is in the store
  • If the category exists in the store, we denormalize it (populate all children / grandchildren / etc categories) before returning
  • If the category does not exist, we return saying that we do not have that information. (This case would be handled in the effect where it checks if the category is in the store. If not, it makes an API call to get it)
Flow visualization

Let us see how we can use normalizr to normalize and denormalize the category structure.

Normalizing:

Firstly, we need to create schemas for our entities. The entity which we would like to normalize is subcategories, which is an array of descendant categories. Therefore, we need to create a schema for the category first, say categorySchema and then we define subcategories as an array of the categorySchema to make it recursive.

With this schema structure, our normalized categories will be an object named categories in the normalized response and subcategories for each category will be an array of ids of the subcategories.

Then, it is just simply a matter of calling normalize with the category information and the category schema.

If we pass in the category details for Mobile Phones to this method, it would create a flattened map of Mobile Phones and all its categories and will look like:

Effect:

Now that the normalizing is all ready, we can use it in our effect to normalize the API response. As described earlier we check if it exists in the store using a selector. If it does, we return that information; otherwise we make an API call, normalize the response and dispatch an action to update the store. We will get to the selector part soon.

Reducer

In our reducer, we would iterate through the normalized category data and store the category and its subcategories in the store against their category ids, making sure not to replace the whole state with the normalized category data for the category requested.

This is the normalization part done.

Denormalizing:

The signature for denormalize is denormalize(input, schema, entities).

input: (required) The normalized result that should be de-normalized.

schema: (required) A schema definition that was used to get the value for input.

entities: (required) An object, keyed by entity schema names that may appear in the denormalized output. Also accepts an object with Immutable data.

To be able to denormalize the normalized data, we need the category that needs to be denormalized, the schema that we used to normalize and the denormalized data.

Selector

Denormalization happens in the selector which is what the components use to get the state information. In the selector, we would check the cache to see if the category id exists. If it does, we denormalize the data corresponding to the id and return it. If it does not exist, we just say that we do not have the information.

This will return the category information just like how we got it from the API.

Adding extra information to the entity

Let us say, the API to fetch the category details has the capability to return category information with certain depth. For example, if we request Mobile Phones with depth = 0, then we would only get the information about Mobile Phones and not any information about its subcategories. For depth = 1, it returns the information about its subcategories but not their subcategories and so on.

Mobile phones requested with depth 0
Mobile phones requested with depth 1
Mobile phones requested with depth 2

The depth for each category varies, some categories are deeper than the others. Whether or not a category has subcategories can be deduced from the flag — isLeaf. If isLeaf is true, the category does not have any subcategories and if it false, the category does have subcategories.

Let us say we have Mobile Phones with depth 1 in our store already.

Information about Mobile phones in the store

When information about Google with depth 0 is requested, we can return it from the store as it already exists. But, when we request information about Google with depth 1, we do not have the required information as the one in the store is only of depth 0 (does not have subcategories).

In this case we need to do 2 things:

  1. Make an API call to get the extra information
  2. Slot the information we got from the API into the store

This way we only request the information we don’t have and not duplicate the API call or the store.

But the problem is the attribute depth does not exist on the API response. In order for us to optimize the API calls and the store, we need to know the depth of the information about the each category we have in the store.

processStrategy

normalizr lets us add a new attribute called depth on each category before we save it in the store by passing in processStrategy while creating an Entity. processStrategy allows us to add extra data or default values to the entity during normalization.

The signature for Entity is Entity(key, definition = {}, options = {}).

options can take in idAttribute, mergeStrategy and processStrategy.

  • idAttribute can be used to derive the id from an attribute on category if it is not a number.
  • mergeStrategy can be used when merging two entities with the same id value.
  • processStrategy allows us to add extra data or default values to the entity.

The signature for processStrategy is processStrategy(value, parent, key).

  • value: The input value of the entity.
  • parent: The parent object of the input array.
  • key: The key at which the input array appears on the parent object.

Firstly, we create an interface that will have both the category information and depth in it:

In our process strategy, we want to calculate the depth of each category and subcategory we are going to normalize. For the parent category, the depth would be the same as the depth we requested while making the API call. For its immediate subcategories, the depth would be one less. For the subsequent subcategories, we keep decrementing the depth. And we would end up with the last level subcategories with depth 0.

The categorySchema with process strategy would now look like:

We could now use the categorySchema to normalize the data and the way to call normalize of normalizr is to pass in the category information with the requested depth and the categorySchema.

The normalized category information would now look like:

comp

With this structure if we want to get information about Google Pixel with a certain depth, it is a straight forward look up in the normalized data structure to see if the depth of the category in the store is greater than or equal to the depth requested. If so, return the information, otherwise, make an API call.

In the effect which handles the request to get category details, we would first check the store to see if the data requested is already there. If so, we return that information and do not make an API call. If not, then we make a call to the API, normalize the response and dispatch an action to update the store with that information.

The reducer will look like below and makes sure that the state for category is not replaced but is updated with the new category details instead.

In the selector, we would check the cache to see if the category id exists. If it does, we check if its depth is greater than or equal to the depth requested. If so, we denormalize the data corresponding to the id and return it. Otherwise, we just say that we do not have the information.

Conclusion

It is a best practice to flatten the state so we can have increased searching and updating performance of the app and a simpler and neater code base. Using the normalizr library seems to be a neat solution to achieve this and also helps us keep the code nice and clean. For information about more ways to use it, refer to the documentation for normalizr.

Thank you for reading! If you enjoyed this article, please feel free to 👏 and help others find it. Please do not hesitate to share your thoughts in the comments section below. Follow me on Medium or Twitter for more articles. Happy coding folks!! 💻 ☕️

--

--