Normalizr-Immutable is an opiniated immutable version of Dan Abramov's Normalizr using Facebook's Immutable. We recommend reading the documentation for Normalizr and Immutable first, to get a basic idea of the intent of these concepts.
npm install --save normalizr-immutable
Based on user feedback I decided to make some changes to the API:
reducerKey
is now an attribute for Schema. This makes it possible to reference entities that are stored in other reducers.
It does mean that if you receive different levels of detail for a single type of entity across REST endpoints, or you want to maintain the original functionality of referencing entities within one reducer, you may need to maintain different Schema definitions for that entity.
If you do want to maintain entities across reducers, you have to be careful not to reference a reducer through the Proxy that has not been hydrated yet.
- The Record object is now part of the method signature for Schema. Since it's not optional, it shouldn't be an option.
- added a new option
useMapsForEntityObjects
to theoptions
object, which defaults tofalse
. WhenuseMapsForEntityObjects
is set totrue
, it will use a Map for the entity objects (e.g. articles). When set tofalse
, it will use a Record for this. See the API description for more info.
normalize(json.articles.items, arrayOf(schemas.article),{
getState: store.getState,
useMapsForEntityObjects: true
});
- added a new option
useProxyForResults
to theoptions
object, which defaults tofalse
. WhenuseProxyForResults
is set totrue
, it will set a Proxy also in the result key object orList
. This will allow you to reference the object directly from the result.
normalize(json.articles.items, arrayOf(schemas.article),{
getState: store.getState,
useProxyForResults: true
});
It normalizes a deeply nested json structure according to a schema for Redux apps and makes the resulting object immutable. It does this in a way that preserves the ability to reference objects using traditional javascript object notation. So, after normalizing an object, you can still reference the tree in the normalized object like a traditional javascript object:
Before normalization
"article": {
"id": 1,
"txt": "Bla",
"user":{
"id":15,
"name":"Marc"
}
}
After normalization
const normalized = {
entities:{//Record with keys: articles, users
articles: {//Record with keys: 1
1: {//Record with keys: id, txt, user
id:1,
txt: 'Bla',
user: 15 //Optionally a proxy
}
},
users:{//Record with keys: 15
15:{//Record with keys: id, name
id:15,
name:'Marc'
}
}
},
result:[1]//List
}
If you use Redux, it optionally, allows you to reference the normalized object through a proxy. This should also work in other environments, but this has not been tested. This allows you to say:
normalized.entities.articles[1].user.name
- Normalizr-Immutable is immutable
- Normalizr puts an id in the place of the object reference, Normalizr-Immutable (optionally) places a proxy there so you can keep using the object as if nothing changed.
- Normalizr-Immutable adds an extra attribute to a schema: Record. This is an Immutable Record that defines the contract for the referenced 'class'.
- Because each Schema uses a Record to define its contract, there is a clearly understandable and visible contract that any developer can read and understand.
- Because Record defines defaults, any unexpected changes to the incoming json structure will be less likely to create unforeseen errors and it will be easier to identify them.
- It gives you the benefit of immutability without sacrificing the convenience of object.property access.
- When you render your data, you don't want to retrieve objects separately for normalized references or marshal your normalized object back into a denormalized one. The use of the Proxy allows you to use your normalized structure as if it was a normal object.
- You can transition to using immutable with minimal changes.
- If you use the proxy, you can serialize a normalized structure back to its original JSON structure with
normalized.toJSON()
.
Normalizr-Immutable uses Records where possible in order to maintain object.property style access. Sequences are implemented through Lists. If you defined an object reference on your to-be-normalized object, it will be processed as a Record if the property has a Schema defined for it. Otherwise, it will become a Map (and require object.get('property') style access).
When you work with Lists and Maps, such as with loops, you should use es6 style .forEach
, .map
, etc. Using for...in
, for...of
and the like will not work.
If you use the useMapsForEntityObjects: true
option when you normalize an object, the entity objects will be stored in a map, to allow you to merge new values into them. Be aware, that Map convert id keys to strings.
const normalized = {
entities:{//Record with keys: articles, users
articles: {//Map with keys: '1'
'1': {//Record with keys: id, txt, user
id:1,
txt: 'Bla',
user: 15 //Optionally a proxy
}
},
users:{//Map with keys: '15'
'15':{//Record with keys: id, name
id:15,
name:'Marc'
}
}
},
result:[1]//List
}
Creating a schema is the same as originally in Normalizr, but we now add a Record to the definition. Please note that you need to use arrayOf, unionOf and valuesOf of Normalizr-Immutable.
import { Record, List, Map } from 'immutable';
import { Schema, arrayOf } from 'normalizr-immutable';
const User = new Record({
id:null,
name:null
});
const Tag = new Record({
id:null,
label:null
});
const Article = new Record({
id:null,
txt:null,
user: new User({}),
tags: new List()
});
const schemas = {
article : new Schema('articles', Article),
user : new Schema('users', User),
tag : new Schema('tags', Tag)
};
schemas.article.define({
user : schemas.user,
tags : arrayOf(schemas.tag)
});
Normalizing data is executed as follows.
import { normalize, arrayOf } from 'normalizr-immutable';
const normalized = normalize(json, arrayOf(schemas.article),{});
Normally, if you normalize an object, the resulting structure will look something like this (All the Object definitions except for List
are Record
implementations).
new NormalizedRecord({
result: new List([1, 2]),
entities: new EntityStructure({
articles: new ValueStructure({
1: new Article({
id : 1,
title : 'Some Article',
user : 1,
tags : new List([5])
})
}),
users: new ValueStructure({
1: new User({
id: 1,
name: 'Marc'
})
}),
tags: new ValueStructure({
5: new Tag({
id:5,
label:'React'
})
})
})
})
So, if you're rendering an Article, in order to render the associated user, you will have to retrieve it from the entity structure. You could do this manually, or you could denormalize/marshal your structure when you retrieve it for rendering. But this can be expensive.
For this purpose, we introduce the proxy. The idea, is that you can simply reference articles[1].user.name
. The proxy will take care of looking up the related object.
Please note that Proxy
support is not yet consistent across browsers and can also give headaches in testing environments with incomplete support (I've had stuff like infinite loops happen using node-inspector, etc). Tread with care.
So, with the proxy, an Article Record essentially looks like this:
new Article({
id : 1,
title : 'Some Article',
author : new Proxy({id:1}),
tags : new List([new Proxy({id:5})])
})
In order to use the proxy, you will need to give it access to the actual object structure. We have developed this feature testing against Redux, so we pass it the getState function reference and the reference to the reducer inside the state structure.
const schemas = {
article : new Schema('articles', Article, { idAttribute: 'id', reducerKey: 'articleReducer' }),
user : new Schema('users', User, { idAttribute: 'id', reducerKey: 'userReducer' }),
tag : new Schema('tags', Tag, { idAttribute: 'id', reducerKey: 'tagReducer' })
};
const normalized = normalize(json.articles.items, arrayOf(schemas.article),{
getState,
reducerKey:'articleReducer'
});
Please note that we pass getState
and not getState()
. getState
is a function reference to the method that will return the current state of the Redux store. If you are using Redux, you can get a reference to this method like so
export function loadArticles(){
return ( dispatch, getState) => {
[...]
const normalized = normalize(json, schema,{
getState
});
[...]
}
}
articleReducer
in this case, is the name of the reducer. Currently we assume that the result
and entitites
keys are available in the root of the referenced reducer. This will be made more flexible in future versions.
This library has currently only been tested against React-Native, so I would like to hear about experiences in the browser. For a list of browsers with appropriate Proxy support http://caniuse.com/#feat=proxy.
This API Reference borrows heavily from the original Normalizr project.
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.
The record
parameter lets you specify the Record that defines your entity.
const User = new Record({
id:null,
nickName: null,
});
const Article = new Record({
//base comment
id:null,
txt:null,
author:new User(),
});
const article = new Schema('articles', Article);
// You can use a custom id attribute
const article = new Schema('articles', Article, { idAttribute: 'slug' });
// Or you can specify a function to infer it
function generateSlug(entity) { /* ... */ }
const article = new Schema('articles', Article { idAttribute: generateSlug });
Lets you specify relationships between different entities.
const article = new Schema('articles', Article);
const user = new Schema('users', User);
article.define({
author: user
});
Returns the key of the schema.
const article = new Schema('articles', Article);
article.getKey();
// articles
Returns the idAttribute of the schema.
const article = new Schema('articles', Article);
const slugArticle = new Schema('articles', Article, { idAttribute: 'slug' });
article.getIdAttribute();
// id
slugArticle.getIdAttribute();
// slug
Returns the Record of the schema.
const article = new Schema('articles', Article);
article.getRecord();
// Article Record object
Describes an array of the schema passed as argument.
const article = new Schema('articles', Article);
const user = new Schema('users', User);
article.define({
author: user,
contributors: arrayOf(user)
});
If the array contains entities with different schemas, you can use the schemaAttribute
option to specify which schema to use for each entity:
const article = new Schema('articles', Article);
const image = new Schema('images', Image);
const video = new Schema('videos', Video);
const asset = {
images: image,
videos: video
};
// You can specify the name of the attribute that determines the schema
article.define({
assets: arrayOf(asset, { schemaAttribute: 'type' })
});
// Or you can specify a function to infer it
function inferSchema(entity) { /* ... */ }
article.define({
assets: arrayOf(asset, { schemaAttribute: inferSchema })
});
Normalizes object according to schema.
Passed schema
should be a nested object reflecting the structure of API response.
You may optionally specify any of the following options:
-
useMapsForEntityObjects
(boolean): WhenuseMapsForEntityObjects
is set totrue
, it will use a Map for the entity objects (e.g. articles). When set tofalse
, it will use a Record for this, but this comes at the expense of not being able to merge new entity objects into the resulting Record object. The advantage of using Records, is that you have dot-property access, but if you use the Proxy, the impact on your code ofuseMapsForEntityObjects: true
is really minimal. I recommend using it. -
useProxyForResults
(boolean): WhenuseProxyForResults
is set totrue
, it will set a Proxy also in the result key object orList
. This will allow you to reference the object directly from the result.
const normalized = normalize(json.articles.items, arrayOf(schemas.article),{
getState:store.getState,
useProxyForResults:true
});
//resulting object looks like this
const normalized = {//Record
entities:{
articles: {
1: {
id:1,
txt: 'Bla',
user: new Proxy({id: 15, key: 'users'})//reference to users
}
},
users:{//Record with keys: 15
15:{//Record with keys: id, name
id:15,
name:'Marc'
}
}
},
result:new List([new Proxy({id: 1, key: 'articles'})])
};
console.log(normalized.result.get(0).user.name);//Prints 'Marc'
-
assignEntity
(function): 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 tests and the discussion for a usage example. -
mergeIntoEntity
(function): You can use this to resolve conflicts when merging entities with the same key. See the test and the discussion for a usage example.
const article = new Schema('articles', Article);
const user = new Schema('users', User);
article.define({
author: user,
contributors: arrayOf(user),
meta: {
likes: arrayOf({
user: user
})
}
});
// ...
// Normalize one article object
const json = { id: 1, author: ... };
const normalized = normalize(json, article);
// Normalize an array of article objects
const arr = [{ id: 1, author: ... }, ...]
const normalized = normalize(arr, arrayOf(article));
// Normalize an array of article objects, referenced by an object key:
const wrappedArr = { articles: [{ id: 1, author: ... }, ...] }
const normalized = normalize(wrappedArr, {
articles: arrayOf(article)
});
The use of the Proxy as a way of accessing the entity structure transparently, would be totally possible also in the original Normalizr library as well. I'm still studying on ways to override functions in a non class structure. If anyone has any suggestions on this, I could spin off the Proxy functionality into a separate library that could serve both libraries.
The way I turn a list of entities into Records (the ValueStructure Record) is a bit of a hack. I basically create the Record with the actual values as defaults, which is not the way you should be using Records. I apply this hack to ensure that we can keep referencing objects through dot notation. If someone has any problems with this in terms of performance, I would like to hear about it.
This library has been developed as part of Ology, the social network for physicians.
I removed harmony-reflect because it's a rather big library and more recent versions of v8 don't need it. I'm just maintaining the harmony-proxy shim.
- Verify working of unionOf and valuesOf. I haven't really worked with that yet.
- If you get any error message with regards to the Proxy object being unknown, please make sure you have set up your babel presets correctly to support proxies. If you use mocha for testing, you will need to add
--harmony-proxies
to the mocha command - If you get unexpected results, please check that you are not accidently using arrayOf, unionOf and valuesOf of the original Normalizr library. Because this library doesn't export some of the components these functions use, I had to copy them and they will fail instanceof even though they are functionally equivalent.