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

Redux Normalizr.js: How to Organize Data in Stores

DZone's Guide to

Redux Normalizr.js: How to Organize Data in Stores

In this article, we go over the Redux store Normalizr.js, and how one team of web developers used it to store their data in the front-end of their app.

· Web Dev Zone
Free Resource

Should you build your own web experimentation solution? Download this whitepaper by Optimizely to find out.

When you work with a React app, it normally needs some data from the server to store for immediate use (e.g. show it on the page). If the app works with some complex relational database the work, then, may be a bit challenging. In this article, I am going to describe some typical issues teams face when organizing data, which we faced during our work on one of DashBouquet's projects. I will also discuss our solutions.

To make it easier, we will have a look at an example. Let’s think of an app with multiple pages, where each page needs a certain amount of data from the server. In some cases, the amount of data would be too massive to be fetched all at once while loading the app. So the data is requested for every page load. And for every page, we most probably have a partition where we can save page-related data, so it would be a good idea to put our fetched data there, too.

In our example, the app has a few pages. The first page is a list of teachers in a university and the second page is a list of students. When the user clicks on a teacher's name, they will be redirected to that teacher's page that also has a list of the students and courses they teach. When a user clicks on a student, they will get redirected to that student's page that has a list of their teachers and courses.

The ER diagram would look like this:

Entity-Relationship Diagram

Fig. 1. Entity-Relationship Diagram

We can decide what data we need on which page:

Division of entities by pages

Fig. 2. Division of entities by pages

To describe my example I will use the stack that we deployed in our project. It includes Loopback for the back-end, React, Redux, and Redux-saga for the front-end, and Axios for the interactions with the server. 

It is clear that same data may be required for different pages. For example, we will have all the teachers on a Teachers page and some of them on the page of a single student. Where do we store them? Maybe we don’t always need to fetch the teachers on Single student page? But if we decide not to fetch the teachers, where do we take the data? If we decide to fetch the data for both pages, where do we put the data to avoid duplication? When the data is spread across the whole store it becomes very confusing, especially if the app is big (more than four pages) and has a large amount of data.

Keeping all the data in one place may help you a lot. But there is a question: how would you fetch it? Surely, we don’t want to fetch the entities separately because this way we would easily reach the point of 30-50 requests per page and this would make page loading awfully slow. We want to try and get as much data as possible from one request (e.g. using an include filter in the case of Loopback). In the response for teachers we get something like this:

[{
        students: [{
                id,
                name,
                studentCourses: [{
                        studentId,
                        courseId,
                        grande,
                    },
                    ...
                ],
            },
            ...
        ],
        courses: [{
                id,
                name,
            },
            ...
        ],
    },
    ...
]

In case you use Loopback as well, have a look at this doc where you can read about how to combine data in queries here.

In this case, our actions would spare us two requests, but this is not very useful if we're storing it on the front-end. First of all, what would happen if something changes in the database (like adding new courses for a teacher)? Instead of re-fetching the courses, we would have to re-fetch both courses and teachers. Secondly, we might need courses inside of a student entity instance. But when we make a query to the server, we don’t include these courses to avoid duplication.

To deal with these issues we can start with Normalizr - a utility that normalizes data with nested objects, just like in our case. No need to describe it in detail: you can find more information here. The point is that after applying some simple manipulations to the result of Normalizr’s work, we get data that we can keep in store.

We need to define a couple of sagas. If you haven’t used redux-saga in your projects yet, I think this should convince you to do so.

The first saga will fetch data:

const urls = {
    teachers: '/teachers,
    students: '/students’,
    courses: '/courses',
};

export function* fetchModel(model, ids, include) {
    const url = urls[model];
    const where = {
        id: {
            inq: ids
        }
    };
    const filter = {
        where,
        include
    };
    const params = {
        filter
    };
    return yield get({
        url,
        params
    });
}

The second will store the data:

export function* queryModels(modelName, ids, include]) {
       const singleModelSchema = schema[modelName];
       const denormalizedData = yield fetchModel(modelName, ids, include);
       const normalizedData = normalize(denormalizedData, [singleModelSchema]);

const {entities} = normalizedData;
yield put(addEntities(entities));
}

And a reducer will add new pieces of data to the already fetched ones:

case ADD_ENTITIES: {
     const models = action.entities;
     const newState = cloneDeep(state);
     return mergeWith(newState, models);
}

The methods mergeWith and cloneDeep used here are from Lodash.

Having done all that we can by querying the data from the server in this manner (selector):

export function* fetchTeachers() {
    yield queryModels('teachers', ids, [{
            relation: 'students',
            scope: {
                include: ['studentCourses'],
            }
        },
        {
            relation: 'courses',
        }
    ]);
}

Normalizr uses a normalization schema as described here.

Eventually, we end up with state that looks like this:

state: {
       ...
       models: {
                   teachers: {...},
                   sudents: {...},
                   courses: {...},
                   studentCourses: {...},
       },
       ...
}

This is actually a pretty small part of our database in the store. There is no need to dispatch a bunch of actions to put fetched data into different sections of the store and to remember where every piece of data should be stored. This is done using the queryModels saga and we always know where the fetched data is going to be put.

After that, we can use it on any page of the app, combing it in selectors as required.

In our case, if needed, we can get an object for a teacher as complicated as this (denormalized data):

{
    students: [{
            id,
            name,
            studentCourses: [...],
            courses: [...],
        },
        ...
    ],
    courses: [{
            id,
            name,
            sudents: [...],
            teachers: [...],
        },
        ...
    ],
}

There is another way as well. We can describe an all-purpose API to denormalize the data before using it. The problem here is that we need an API to denormalize data because the denormalize function that comes along with the Normalizr package would only denormalize data to the shape it was in when it came from the server, which is not exactly what we want. As described above, we got courses only within teacher entities, though we might need them anywhere else. In the case of larger projects, I believe you may want to spend time on a custom denormalization function. However, this is a different topic and we may cover it in the next article.

For me, this approach was quite a relief. The main advantage here is that you always know where to find what you need. And in case you decide that it would be better to aggregate data in another way, you don’t need to mess with the queries again, you just change a selector a little. In general, managing data in store requires much less effort with this approach.

Implementing an Experimentation Solution: Choosing whether to build or buy?

Topics:
redux ,javascript ,data store ,normalization ,web dev

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}