Using Normalizr to organize data in store. Part 2

Written by dashmagazine | Published 2017/10/17
Tech Story Tags: javascript | react | normalizr | redux | api

TLDRvia the TL;DR App

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 1 we had an example with the entity relations described by this diagram:

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 _cloneDee_p 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 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 comment the article.

Missing part of Redux Saga Experience_Redux saga is a middleware between an application and redux store, that is handled by redux actions. This means, it can…_hackernoon.com

Using Normalizr to organize data in stores — practical guide_After applying few simple manipulations to the result of Normalizr’s work we get data that we can keep in store_hackernoon.com

Usage of Reselect in a React-Redux Application_Why reselect is so good_hackernoon.com

How to Stop Using Callbacks and Start Living_Javascript has two major ways of dealing with asynchronous tasks — callbacks and Promises. In general Promises are…_hackernoon.com

Written by Ilya Bohaslauchyk


Written by dashmagazine | Dashbouquet is a web & mobile development agency, helping startups & SMEs build robust web and mobile apps since 2014.
Published by HackerNoon on 2017/10/17