Using Normalizr to Organize Data in Store. Part 2The second part of the article about how to use Normalizr to organize data in stores. This part suggests an all-purpose denormalizing API to be used with the Normalizr library.
Oct 13 2017 | by Ilya Bohaslauchyk

In this article, I want to continue on the topic of using Normalizr in a React-Redux application and finally answer the question about an all-purpose API which was mentioned briefly in the previous article.

To remind you what that article was about, Normalizr is a utility that normalizes data represented by nested entities (like in server responses), so that it can be stored and used later just as if there was a copy of a database on the front-end (e.g. in the Redux store). In part I we had an example with the entity relations described by this diagram:

Diagram shows entity-relationshop student-course-teacher

Fig. 1. Entity-Relationship Diagram

Normalizr has a denormalization API out of the box, but it may be insufficient because it requires data from the server to be fetched in the exact shape that you want to use in the application. For example, if you want to denormalize data from the example in the previous article so that a student entity includes courses, it has to be fetched exactly this way - courses inside students. But you may fetch courses within teacher entities or just courses separately. In this case, there are basically two ways: you may denormalize the entities in the selectors as was described in part I or you may define your own API which we will try to do here.

The main difficulty with the API is that unlike on the back-end, we don’t have all the models with their relations described on the front-end. Schemas we defined for Normalizr aren’t of much help either, because we don’t denote the relations between the entities there. In fact, front-end doesn’t know anything about entities and relations in the database. So if we want to include something in the model we request from the store, we should explicitly denote how to find what we want to include.

I think it would be good to be able to define a request to the store in this manner:

const schema = {
    modelName: 'student',
    include: [
        {
            modelName: 'studentCourse',
            isRelation: true,
            include: [
                {
                    modelName: 'course',
                    include: [
                        {
                            modelName: 'teacher',
                            through: 'teacherCourse',
                        }
                    ]
                },
            ],
        }
    ]
}

Here we want to get student from the store with the models denoted in the include property to be nested inside. It looks quite similar to the requests we were composing to get data from the server in the part I, only with a couple of additional properties. Here I use a property isRelation to denote that a relation should be included and a property through to tell the API how to find a required entity. We will get to that a bit later. Now we can try to implement the API. It should only have a couple of functions. The first one is called denormalize and should be called directly when using the API:

function denormalize (entity, models, schema) {
    const {include} = schema;
 
    const toInclude = include.map(i => {
        const entities = getInclusion(entity, models, schema, i);
        if (i.include) {
            //if an inclusion has its own inclusion, call the function recursively 
            return entities.map(e => denormalize(e, models, i));
        } else {
            return entities;
        }
    });
 
    const entityWithRelations = {...entity};
    include.map((i, index) => 
        entityWithRelations[i.modelName] = toInclude[index]);
 
    return entityWithRelations;
}

This function should take as parameters an entity to be denormalized (entity) and all the entities from the store (models) to find the ones that have to be nested. Parameter schema here describes a request to the store as defined above. It should be an object of this kind:

{
    modelName, //name of a requested model
    through, //name of a relation through which it should be found
    include, //an array of schemas to be included in the requested model
    isRelation, //true, if the requested model is a relation
}

And the second function should get the entities to be nested from the models object. Here I made use of the fact that in a relation ids of the related models are stored as modelName Id:

function getInclusion(entity, models, modelSchema, inclusionSchema) {
    const {modelName, isRelation: isModelRelation} = modelSchema;
    const {modelName:inclusionName, through, isRelation} = inclusionSchema;
 
    if (isModelRelation) { //include into a relation
       const foreignKey = `${inclusionName}Id`;
 
               return values(models[inclusionName])
        .filter(m => m.id === entity[foreignKey]);
    } else { //include into an entity
        if (isInclusionRelation) { //include a relation
            const ownKey = `${modelName}Id`;
 
            return values(models[inclusionName])
                .filter(m => m[ownKey] === entity.id);
        } else { //include an entity
            const ownKey = `${modelName}Id`;
            const foreignKey = `${inclusionName}Id`;
 
            const relations = values(models[through])
                .filter(r => r[ownKey] === entity.id);
            return relations.map(r => models[inclusionName][r[foreignKey]]);
        }
    }
}

We have to think about three cases here:

Case 1: include an entity into a relation. In this case, we can find the required entities taking an id from the relation we want to include into.

Case 2: include a relation into an entity. We have to go the opposite way - we find a relation by the entity id.

Case 3: include an entity into another entity. We have to walk through a bit longer way in this case. First, we find a relation by the entity id (just like in the second case) and then we find the required entities taking their ids from the relations.

This is pretty much it. One more thing to mention: on the top of the API as it is in the suggested implementation there should be a selector. If we use reselect, it may look like this:

export const selectModels = (state) => state.models;
 
export const find = (schema, ids) => createSelector (
    selectModels,
    (models) => {
       const {modelName, include} = schema;
       return !include 
           ? values(pick(models[modelName], ids)) 
           : values(pick(models[modelName], ids))
               .map(v => denormalize(v, models, schema));
    }
);

So, the actual request from a saga comes down to this shape:

const candidates = yield select(find({schema, ids}))

Methods values, pick and cloneDeep here are from Lodash.

This API was tested by me but never used in a real project so far. Though I think I will apply it as soon as I get an opportunity. It seems to be useful because we don’t need to write thesamedenormalizations in the selectors every time we want data from the store and it is probably easier to use. Anyway, it is more of a suggestion now than a verified and ready to use solution. If you have other thoughts on the subject, please feel free to comment the article. 


More tutorials in this series:

Missing Part of Redux Saga Experience

Using Normalizr to Organize Data in Stores – Practical Guide

Usage of Reselect in a React-Redux Application

How to Stop Using Callbacks and Start Living

Latest news
Software Development
Dashbouquet Development Recognized by GoodFirms as the Best Company to Work With
In the era of immense market competition, innovations such as mobile apps and websites have become a core component of any successful business. From serving customers in real-time to delivering exceptional user experiences, both mobile applications, and websites have become a necessity to all businesses, irrespective of the size to increase the revenue, market share and profitability of the businesses. It all rounds up to choosing the best, and the top software development companies that can help businesses stand out from the competition.
Dashbouquet Development Recognized by GoodFirms as the Best Company to Work With
News
Dashbouquet Development Recognized as a Top Performer by Aciety in Multiple Categories
Dashbouquet Development is thrilled to announce its recent nominations and badges from Aciety
Dashbouquet Development Recognized as a Top Performer by Aciety in Multiple Categories
News
Dashbouquet Development Honored as a Clutch Champion for 2023
New award under our belt! Dashbouquet Development Honored as a Clutch Champion for 2023
Clutch Award 2023. Dashbouquet Development Honored as a Clutch Champion