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;
}
});