I'm porting an existing app to Flux and I'm a bit confused about one topic.
Say I have several API endpoints that return two- or three-level nested objects.
For example, GET /articles
may return a JSON response of schema
articles: article*
article: {
author: user,
likers: user*
primary_collection: collection?
collections: collection*
}
collection: {
curator: user
}
As you see, there are all kinds of users at different levels of nesting:
articles[i].author
articles[i].likers[i]
articles[i].primaryCollection.curator
articles[i].collections[i].curator
If I want to update UserStore
with fresh data any time articles are fetched, I'd have to write a monstrous method that checks all nested entities on article API response. Moreover, there would be a lot of duplication because there are also other API endpoints with different schemas, and sometimes articles are embedded inside users (e.g. GET /user/published
).
Is there a cleaner way for Flux stores to extract nested entities out of all API responses?
An approach suggested by Jing Chen (one of Flux creators and evangelists) was to flatten API responses before they reach the Stores. I wrote a small library that does just that: it normalizes
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
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 there is no duplication and the structure is flat.)
Normalizr lets you:
- Nest entities inside other entities, objects and arrays
- Combine entity schemas to express any kind of API response
- Automatically merge entities with same IDs (with a warning if they differ)
- Use a custom ID attribute (e.g. slug)
To use it, you need to define your entities and nesting rules and use them to transform JSON:
var normalizr = require('normalizr'),
normalize = normalizr.normalize,
Schema = normalizr.Schema,
arrayOf = normalizr.arrayOf;
// First, define a schema:
var article = new Schema('articles'),
user = new Schema('users'),
collection = new Schema('collections');
// Define nesting rules:
article.define({
author: user,
collections: arrayOf(collection)
});
collection.define({
curator: user
});
// Usage:
// Normalize articles
var articlesJSON = getArticleArray(),
normalized = normalize(articlesJSON, arrayOf(article));
// Normalize users
var usersJSON = getUsersArray(),
normalized = normalize(usersJSON, arrayOf(user));
// Normalize single article
var articleJSON = getArticle(),
normalized = normalize(articleJSON, article);
This allows you to normalize any XHR response before passing it to Flux Dispatcher.
The Stores will only need to update themselves from the corresponding dictionary:
// UserStore
UserStore.dispatchToken = AppDispatcher.register(function (payload) {
var action = payload.action;
switch (action.type) {
// you can add any normalized API here since that contains users:
case ActionTypes.RECEIVE_ARTICLES:
case ActionTypes.RECEIVE_USERS:
// Users will always be gathered in action.entities.users
mergeInto(_users, action.entities.users);
UserStore.emitChange();
break;
}
});
// ArticleStore
AppDispatcher.register(function (payload) {
var action = payload.action;
switch (action.type) {
// you can add any normalized API here since that contains articles:
case ActionTypes.RECEIVE_ARTICLES:
// Wait for UserStore to digest users
AppDispatcher.waitFor([UserStore.dispatchToken]);
// Articles will always be gathered in action.entities.articles
mergeInto(_articles, action.entities.articles);
ArticleStore.emitChange();
break;
}
});