背景
reduxとnormalizr使ってます、という話を勉強会でした時に、いまいち実装イメージが思い浮かばないというフィードバックをもらったので、TODOアプリで例を作ってみた。
ソースはこちら:https://github.com/hokuma/redux-normalizr-todo
TL;DR
- APIのレスポンスでリソースがネストしている時に効果的
- entities(ローカルDB)とresult(データの集合)でreducerを分ける
- entitiesはImmutable.Record使う(おまけ)
題材
よくあるTODOアプリだとリソースはTODOしか出てこない。リソースが一つしかなければそもそもReact単体でやったら、という話になるので、TODOを実行する人をアサインできる機能を持ったTODOアプリを題材にした。
書くこと
reducerの切り方、normalizrの使い方、entityの持ち方。
書かないこと
action周り。
リソース
扱うリソースは以下の通り。
{
"todos":[
{
"todo":{
"id":1,
"body":"ticket #1",
"status":1,
"userId": 1,
"user":{
"id":1,
"name":"john"
}
}
}
]
}
{
"users":[
{
"user":{
"id":1,
"name":"john",
"status":1
}
}
]
}
クライアント側での描画待ちを減らしたい、などの理由で一つのAPIレスポンス内でリソースがネストするようなケースは実際あるだろう。
normalizr
リンク先を見ればわかるが、ネストしたjsonをentityとresultに分解してくれるライブラリ。
entityはデータの実体で、データベースの1レコードに相当すると考えれば良い。resultは意味を持ったデータの集合だが、実データは持たずentityへの参照のみを持つ。この辺は説明よりもリンク先の例を見た方がわかりやすい。
実体とそれへの参照に分けるところがポイントになる。
One Fact in One Place
をクライアント側でもやる感じ。
reducer
entitiy用、result用のreducerを作る。entity用はユーザ用、TODO用とそれぞれネストしていく。result用も同様で、ユーザ一覧、TODO一覧用とネストする。
- entitiy用のreducerはクライアント側のテーブル操作を行うreducer
- result用のreducerはアプリとして見せたい情報のまとまり(ユーザ一覧、TODO一覧)操作を行うreducer
と見ることができる。
entities
entities/
以下にそれぞれのリソースごとに定義する。
アクションごとに適用するデータ処理を定義する。データ取得系では、基本的にはサーバから受け取った値を最新の値とみなし、既存データを更新 or なければローカルのstoteに追加する。これをまとめてmerge
処理として実装する。
状態更新系では、例えばfinish
のような状態変化の操作を定義し、対象のtodoのstatus
を更新する。
result
entities/
以外のreducerは、基本的にはresultを処理するreducerとなる。
取得したデータ or 追加したデータのentityIdをリストへマージするのが主な役割。finish
のようなentityそのものを変える処理は扱わない(entityのreducerで処理する)。
Immutable.js
entity、resultどちらもImmutable.jsで管理する。
オブジェクトや集合を扱うためのAPIが豊富で、かつ実行結果が新しいオブジェクトになるのでデータ更新の副作用に起因するバグを減らせる。
データ型がすでにいくつか定義されており、使いようによってはなかなか便利。例えば、resultでOrderedSet
を使っている。Set
なので同一の値は勝手にユニークが取られ、さらにOrdered
、つまり追加した順番を保持してくれるので、一覧表示のためのデータを管理するのに都合が良い。
Immutable.Record
Immutable.Recordを使って、Immutableのインスタンスをクラスのインスタンスっぽく使うこともできる。できることは以下の3つ。
- 取りうる属性とdefault値を定義できる
- 属性名でアクセスできる(
get
を使わなくて良い) - 任意のgetterを定義できる
特に2つ目と3つ目のおかげで、Immutable.jsを使いつつも普通のオブジェクトっぽく使えるので、Component内からのアクセスが簡潔になる。