Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Redux Normalizr.js: How to Organize Data in Stores, Part 2

DZone's Guide to

Redux Normalizr.js: How to Organize Data in Stores, Part 2

In this article, we learn how to use this React-Redux app with an API to be able to get access to data and organize it in our web app.

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

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:

Fig. 1. Entity-Relationship Diagramge title

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, the front-end doesn’t know anything about entities and relations in the database. So if we want to include something in the model that we requested 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 a 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 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 an entity to be denormalized (entity) as its parameters, and all the entities from the store (models) to find the ones that have to be nested. The 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 model's 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.

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 go through a bit longer of a process 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 by taking their ids from the relations.

That 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 used here are from Lodash.

This API was tested by me but has not been 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 the same denormalizations 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 leave a comment on the article.

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:
react ,redux ,normalization ,front end development ,web dev

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}