Redux Advent Calendar 2017の23日目の記事です。
reduxのstoreのデータを正規化する時にreselectとnormalizrを使うのが一般的なのですが、immutable.jsでmodelを作成してるパターンの解説記事がなかったので書きます。
時間がなく疑似コード、省略ありで書きますので、動く保証はしません。
{
id: "123",
author: {
id: "1",
name: "Paul"
},
title: "My awesome blog post",
comments: [
{
id: "324",
commenter: {
id: "2",
name: "Nicole"
}
}
]
}
{
"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" }
}
}
疑似コード
store.dispatch(initArticle(articleParams))
componentなどでactionを実行する(今回は非同期処理など面倒なところは省くが、本番だとほぼ非同期でとってきたデータが正規化される)
import { normalize, schema } from 'normalizr';
// Define a users schema
const userSchema = new schema.Entity('users');
// Define your comments schema
const commentSchema = new schema.Entity('comments', {
commenter: user
});
// Define your article
const articleSchema = new schema.Entity('articles', {
author: user,
comments: [ comment ]
});
export function initArticle(articleParams) {
return {
type: 'article/initialize',
meta: {
schema: ArticleSchema,
},
payload: {
...article,
},
};
}
import { normalize } from 'normalizr';
export function normalizrMiddleware(): {
return (store) => (next) => (action) => {
const schema = action.meta && action.meta.schema;
if (schema && action.payload && !action.error) {
const normalized = normalize(action.payload, schema);
action = { ...action, payload: normalized };
}
return next(action);
};
}
middlewareでschemaとpayloadがあれば正規化する。reducerはentitiesに正規化したデータを入れて、resultをarticleのreducerで保存しておく。
import { fromJS, Map } from 'immutable';
import { schemas } from '../schemas';
const hash = {};
Object.keys(schemas).forEach((key) => {
hash[key] = Map({});
});
const INITIAL_STATE = fromJS(hash);
export function entities(state = INITIAL_STATE, action) {
if (action.payload && action.payload.entities) {
let newMap = Map();
Object.entries(action.payload.entities).forEach(([modelName, modelMap]) => {
let newModelMap = Map({});
Object.entries(modelMap).forEach(([key, model]) => {
newModelMap = newModelMap.set(parseInt(key, 10), model);
});
newMap = newMap.set(modelName, newModelMap);
});
return state.mergeDeep(newMap);
}
return state;
}
payload.entitiesにあるものは全て正規化されてる前提で保存していきます。
const INITIAL_STATE = {
articleId: 0,
};
export function articleReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'article/initialize':
return {...state, articleId: action.payload.result };
default:
return state;
}
}
const mapStateToProps = () => {
const getArticle = makeGetArticle();
return (state) => {
return {
article: getArticle(state),
};
};
};
const mapDispatchToProps = () => {
return {};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Component);
ここがポイントでstate.entitiesの段階では正規化されたplain objectだったが、getArticleで渡される値はimmutable.jsのmodel。reselectで合成する。
import { Record } from 'immutable';
const ArticleRecord = new Record({
id: null,
author: null,
title: true,
comments: [],
});
export default class Article extends ArticleRecord {
static fromJS(article = {}) {
const comments = (article.comments || []).map((comment) => {
return Comment.fromJS(comment);
});
return (new this(article)).merge({
comments: comments,
author: new User(article.author),
});
}
}
const getArticlesMap = (state) => state.entities.get('articles');
const getUsersMap = (state) => state.entities.get('users');
const getCommentsMap = (state) => state.entities.get('comments');
const getArticleId = (state) => state.articleReducer.articleId;
export const makeGetArticle = () => {
return createSelector(
getArticleId,
getArticlesMap,
getCommentsMap,
getUsersMap,
(articleId, articlesMap, commentsMap, usersMap) => {
const article = articlesMap.get(articleId)
return Article.FROM_JS({
...article,
comments: article.comments.map((id) => commentsMap(id)),
author: usersMap(article.author),
});
});
};
Q&A
-
変更はどうするの?
- 基本はサーバー通信時にentitiesを書き換え
- ユーザー入力はredux-formやfinal-formを使って別管理
- ローカルのタブ選択などは正規化しなくて良い
-
それ以外でどうしてもentitiesを書き換えたいときは?
- 個別に実行していくしかないですかね。。ベストプラクティスを逆に知りたい。