redux, immutable.js, reselect, normalizrを連携させる

[Redux Advent Calendar 2017](https://qiita.com/advent-calendar/2017/redux)の23日目の記事です。

reduxのstoreのデータを正規化する時にreselectとnormalizrを使うのが一般的なのですが、immutable.jsでmodelを作成してるパターンの解説記事がなかったので書きます。
時間がなく疑似コード、省略ありで書きますので、動く保証はしません。

```js:非正規化されてるarticleParamsデータ
{
  id: "123",
  author: {
    id: "1",
    name: "Paul"
  },
  title: "My awesome blog post",
  comments: [
    {
      id: "324",
      commenter: {
        id: "2",
        name: "Nicole"
      }
    }
  ]
}
```

```json:正規化されてるarticleParamsデータ
{
  "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" }
  }
}
```

## 疑似コード

```js:component
store.dispatch(initArticle(articleParams))
```

componentなどでactionを実行する(今回は非同期処理など面倒なところは省くが、本番だとほぼ非同期でとってきたデータが正規化される)

```js:schema
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 ]
});
```


```js:actions
export function initArticle(articleParams) {
  return {
    type: 'article/initialize',
    meta: {
      schema: ArticleSchema,
    },
    payload: {
      ...article,
    },
  };
}
```

```js:middleware
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で保存しておく。

```js:reducers/EntityReducer.js
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にあるものは全て正規化されてる前提で保存していきます。

```js:reducers/ArticleReducer.js
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;
  }
}
```

```js:container

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で合成する。

```js:models/Article.js
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),
    });
  }
}
```

```js:selector

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を書き換えたいときは？
  * 個別に実行していくしかないですかね。。ベストプラクティスを逆に知りたい。

## Appendix
[Reduxのreselectとは](https://qiita.com/zaki-yama/items/5258e6f1ae37f63034b9)
[reduxのstateを正規化してCQRSの恩恵を受ける土台を作る](https://qiita.com/193/items/94f7159245a9b1607f2d)