Normalizrを使用したReduxの実装パターン

# 課題
APIなどで取得したデータはたいてい深くネストしていることがおおく、reducerでのデータ更新処理が複雑になりがち。
そこでデータを Normalie(平たく)することで実装しやすくする。

[Reduxの公式ドキュメント](http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html)にも実装パターンの例が載っているが、今回は、[Normalizr](https://github.com/paularmstrong/normalizr) を使用した場合の実装パターンを見てみる。

# 使い方
基本的な使い方は[README](https://github.com/paularmstrong/normalizr#quick-start)参照。

例えばデータが元データがこんな形だった場合、

```js
[
  "123": {
    "id": "123",
    "author": {
      "id": "1",
      "name": "Paul"
    },
    "title": "My awesome blog post",
    "comments": [
      {
        "id": "324",
        "text": "Awesome comment",
        "commenter": {
          "id": "2",
          "name": "Nicole"
        }
      }
    ]
  }
]

```
Normalize後は下記のようになる。

```js
{
  results: ["123"],
  entities: {
    "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", text: "Awesome comment", "commenter": "2" }
    }
  }
}
```

# 実装例
初期データはAPIから取得することを想定している。
まず、[redux-saga](https://github.com/redux-saga/redux-saga)や [redux-thunk](https://github.com/gaearon/redux-thunk) などの Redux-middleware で取得したデータをNormalizeして `ARTICLE_FETCH_SUCCESS` アクションを発行する。
今回は `redux-saga` を使用した例。

```sagas.js
import { put, takeEvery, all } from 'redux-saga/effects'
import apiClient from './apiClient'
import { normalize } from 'normalizr'
import schemas from './schemas'

export function* fetchArticle(action) {
  const id = action.id
  // APIで取得したデータをNormalizeする
  const response = yield apiClient.get('/article/' + id)
  const article = schemas.article
  const normalizedData = normalize(response.data, [article])
  yield put({ type: 'ARTICLE_FETCH_SUCCESS', payload: normalizedData })
}

export function* watchArticleFetch() {
  yield takeEvery('ARTICLE_FETCH_REQUESTED', fetchArticle)
}

export default function* rootSaga() {
  yield watchArticleFetch()
}
```

`rootReducer` ではこのアクションを受け取った場合のみ `entities` を取得したデータでそのまま置き換えている。
また、`combineReducer` を使って、 `entities` とその他のステートをスライスする。
データ取得アクション以外では `articlesReducer`、`usersReducer`、`commentsReducer` にそれぞれ処理を移譲する。


```reducers/rootReducer.js
import { routerReducer } from 'react-router-redux'
import { combineReducers } from 'redux'
import articlesReducer from './articles.js'
import usersReducer from './users.js'
import commentsReducer from './comments.js'

const entities = (state = {}, action) => {
  switch (action.type) {
  case 'ARTICLE_FETCH_SUCCESS': {
    const entities = action.payload.entities
    return entities
  }
  default:
    return {
      ...state,
      articles: articlesReducer(state.articles, action),
      users: usersReducer(state.users, action),
      comments: commentsReducer(state.comments, action),
    }
  }
}
export default combineReducers({
  entities,
  routing: routerReducer
  ... // その他reducer
})
```

## 例えばコメントを追加する場合
`actionCreator` では一意な `commentId` を生成してReducerに渡す。

```action.js
export const addComment = (articleId, commenterId, text) => {
  const commentId = generateId() // ユニークなIDを生成する
  return { type: 'ADD_COMMENT', articleId, commentId, commenterId, text }
}
```

`articlesReducer` では、コメントが追加された場合は自身の `comments` プロパティに `commentId` を追加するだけでいい。

```reducers/articles.js
export default (state = {}, action) => {
  switch (action.type) {
  case 'ADD_COMMENT': {
    const { articleId, commentId } = action
    const article = state[articleId]
    const comments = article.comments.concat(commentId)
    return {
      ...state,
      [articleId]: {
        ...article,
        comments
      }
    }
  }
  default:
    return state
  }
}
```
その他にも例えば `comments` の順番を入れ替えたい時は `articles.comments` のidの並びを入れ替えるだけで良い。

`commentsReducer` で、`comment` の実体を生成して `state` に追加する。

```reducers/comments.js
export default (state = {}, action) => {
  switch (action.type) {
  case 'ADD_COMMENT': {
    const { commenterId, commentId, text } = action
    const comment = {
      id: commentId,
      commenter: commenterId,
      text
    }
    return {
      ...state,
      [commentId]: comment
    }
  }
  default:
    return state
  }
}

```

このようにネストしたデータをNormalizeすることでキレイにReducerを分割することが出来る。

# View側の実装
View側ではNormalizeされたままのデータだと使いにくい。
今回は `react-redux` の `mapStateToProps`内でNormalizeされたデータを**Denormalize**した。
Denormalizeすることで通常通り `article.comments` で`comment`の配列にアクセスできる。

```containers/Article.js
import React, { Component } from 'react'
import { denormalize } from 'normalizr'
import { connect } from 'react-redux'
import ArticleForm from './ArticleForm/index'
import { fetchArticle } from '../actions'
import schema from '../schemas'

class Article extends Component {
  componentDidMount() {
    this.props.dispatch(newArticle())
  }

  render() {
    if (!this.props.article) {
      return null
    }
    return <ArticleForm article={this.props.article} />
  }
}

const mapStateToProps = (state, ownProps) => {
  if (!state.entities) {
    return {}
  }
  const { params } = ownProps
  const denormalized = denormalize({ article: params.id }, schema, state.entities)
  return {
    article: denormalized.article
  }
}

export default connect(mapStateToProps)(Article)
```

# 所感
## 良い点
* Reducerの分割がしやすい
* 各Reducerの実装がシンプルになる
* それによりテストが書きやすくなる

## 悪い点
* データ同士の紐付けがIDだよりになるので毎回一意のIDを生成する必要がある
* Viewで毎回Denormalizeするのでパフォーマンスが若干気になる