What is normalizr?
Normalizr is a powerful library for normalizing nested JSON data. It helps in transforming complex nested data structures into a flat structure, making it easier to manage and work with in applications, especially in state management scenarios.
What are normalizr's main functionalities?
Define Schemas
Normalizr allows you to define schemas for your data. In this example, we define schemas for users, comments, and articles, where comments have a relationship with users, and articles have relationships with both users and comments.
const { schema } = require('normalizr');
const user = new schema.Entity('users');
const comment = new schema.Entity('comments', {
commenter: user
});
const article = new schema.Entity('articles', {
author: user,
comments: [comment]
});
Normalize Data
Once schemas are defined, you can use the `normalize` function to transform your nested data into a normalized form. This example shows how to normalize a nested JSON object representing an article with an author and comments.
const { normalize } = require('normalizr');
const originalData = {
id: '123',
author: {
id: '1',
name: 'Paul'
},
title: 'My awesome blog post',
comments: [
{
id: '324',
commenter: {
id: '2',
name: 'Nicole'
}
}
]
};
const normalizedData = normalize(originalData, article);
console.log(JSON.stringify(normalizedData, null, 2));
Denormalize Data
Normalizr also provides a `denormalize` function to convert normalized data back into its original nested form. This example demonstrates how to denormalize data using the previously defined schemas.
const { denormalize } = require('normalizr');
const normalizedData = {
result: '123',
entities: {
articles: {
'123': { id: '123', author: '1', title: 'My awesome blog post', comments: ['324'] }
},
users: {
'1': { id: '1', name: 'Paul' },
'2': { id: '2', name: 'Nicole' }
},
comments: {
'324': { id: '324', commenter: '2' }
}
}
};
const denormalizedData = denormalize('123', article, normalizedData.entities);
console.log(JSON.stringify(denormalizedData, null, 2));
Other packages similar to normalizr
normalizr-immutable
normalizr-immutable is a fork of normalizr that works with Immutable.js data structures. It provides similar functionality to normalizr but is designed to work seamlessly with Immutable.js, making it a good choice for applications that use Immutable.js for state management.
redux-orm
redux-orm is a library for managing relational data in Redux. It provides an ORM-like interface for defining models and relationships, and it integrates with Redux to manage normalized data. Unlike normalizr, which focuses on normalization and denormalization, redux-orm provides a more comprehensive solution for managing relational data in Redux applications.
normalizr
Normalizes deeply nested JSON API responses according to a schema for Flux application.
Kudos to Jing Chen for suggesting this approach.
Sample App
Flux
See flux-react-router-example.
Redux
See redux/examples/real-world.
The Problem
- You have a JSON API that returns deeply nested objects;
- You want to port your app to Flux;
- You noticed it's hard for Stores to consume data from nested API responses.
Normalizr takes JSON and a schema and replaces nested entities with their IDs, gathering all entities in dictionaries.
For example,
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
can be normalized to
{
result: [1, 2],
entities: {
articles: {
1: {
id: 1,
title: 'Some Article',
author: 1
},
2: {
id: 2,
title: 'Other Article',
author: 1
}
},
users: {
1: {
id: 1,
name: 'Dan'
}
}
}
}
Note the flat structure (all nesting is gone).
Features
- Entities can be nested inside other entities, objects and arrays;
- Combine entity schemas to express any kind of API response;
- Entities with same IDs are automatically merged (with a warning if they differ);
- Allows using a custom ID attribute (e.g. slug).
Usage
import { normalize, Schema, arrayOf } from 'normalizr';
First, define a schema for our entities:
const article = new Schema('articles')
const user = new Schema('users');
const collection = new Schema('collections');
Then we define nesting rules:
article.define({
author: user,
collections: arrayOf(collection)
});
collection.define({
curator: user
});
Now we can use this schema in our API response handlers:
const ServerActionCreators = {
receiveArticles(response) {
const normalized = normalize(response, {
articles: arrayOf(article)
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVE_ARTICLES,
normalized: normalized
});
},
receiveUsers(response) {
const normalized = normalize(response, {
users: arrayOf(user)
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVE_USERS,
normalized: normalized
});
}
}
Finally, different Stores can tune in to listen to all API responses and grab entity lists from action.normalized.entities
:
AppDispatcher.register((payload) => {
const { action } = payload;
switch (action.type) {
case ActionTypes.RECEIVE_ARTICLES:
case ActionTypes.RECEIVE_USERS:
mergeUsers(action.normalized.entities.users);
UserStore.emitChange();
break;
}
});
API
####new Schema(key, [options])
Schema lets you define a type of entity returned by your API.
This should correspond to model in your server code.
The key
parameter lets you specify the name of the dictionary for this kind of entity.
const article = new Schema('articles');
const article = new Schema('articles', { idAttribute: 'slug' });
####Schema.prototype.define(nestedSchema)
Lets you specify relationships between different entities.
const article = new Schema('articles');
const user = new Schema('users');
article.define({
author: user
});
####arrayOf(schema)
Describes an array of the schema passed as argument.
const article = new Schema('articles');
const user = new Schema('users');
article.define({
author: user,
contributors: arrayOf(user)
});
####normalize(obj, schema, [options])
Normalizes object according to schema.
Passed schema
should be a nested object reflecting the structure of API response.
You may optionally specify a custom assignEntity
function in options
. This is useful if your backend emits additional fields, such as separate ID fields, you'd like to delete in the normalized entity. See the test and the discussion for a usage example.
const article = new Schema('articles');
const user = new Schema('users');
article.define({
author: user,
contributors: arrayOf(user),
meta: {
likes: arrayOf({
user: user
})
}
});
const json = getArticleArray();
const normalized = normalize(json, arrayOf(article));
Explanation by Example
Say, you have /articles
API with the following schema:
articles: article*
article: {
author: user,
likers: user*
primary_collection: collection?
collections: collection*
}
collection: {
curator: user
}
Without normalizr, your Stores would need to know too much about API response schema.
For example, UserStore
would include a lot of boilerplate to extract fresh user info when articles are fetched:
AppDispatcher.register((payload) => {
const { action } = payload;
switch (action.type) {
case ActionTypes.RECEIVE_USERS:
mergeUsers(action.rawUsers);
break;
case ActionTypes.RECEIVE_ARTICLES:
action.rawArticles.forEach(rawArticle => {
mergeUsers([rawArticle.user]);
mergeUsers(rawArticle.likers);
mergeUsers([rawArticle.primaryCollection.curator]);
rawArticle.collections.forEach(rawCollection => {
mergeUsers(rawCollection.curator);
});
});
UserStore.emitChange();
break;
}
});
Normalizr solves the problem by converting API responses to a flat form where nested entities are replaced with IDs:
{
result: [12, 10, 3, ...],
entities: {
articles: {
12: {
authorId: 3,
likers: [2, 1, 4],
primaryCollection: 12,
collections: [12, 11]
},
...
},
users: {
3: {
name: 'Dan'
},
2: ...,
4: ....
},
collections: {
12: {
curator: 2,
name: 'Stuff'
},
...
}
}
}
Then UserStore
code can be rewritten as:
AppDispatcher.register((payload) => {
const { action } = payload;
switch (action.type) {
case ActionTypes.RECEIVE_ARTICLES:
case ActionTypes.RECEIVE_USERS:
mergeUsers(action.normalized.entities.users);
UserStore.emitChange();
break;
}
});
Dependencies
- lodash for
isObject
and isEqual
Installing
npm install normalizr
Running Tests
npm install -g mocha
npm test