54
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

reducer の ノーマライゼーション

Last updated at Posted at 2016-09-04

本来は実体を配列で保持するようなケースを、次のような構成に分ける

reducerが返すある状態を、全体を保持するオブジェクトと、全体の各IDを保持する配列に分ける。
全体を保持するオブジェクトのキーを指定して実体を手に入れる。まとまりをそのまま処理する場合は、全体の各IDを保持する配列にmap()を適用し、必要な実体のリストを手に入れる。

なぜ、こうするのか

redux のアプローチでは、情報を管理するストアはトップレベルに一つだけある。そしてこのストアは意味のあるデータの塊ごとに reducer を作成し、必要に応じて combineReducers することで合成することができる。擬似的には

Store: {
 Products :: array,
 Cart :: array,
}

のような感じで、配列を使って表される。
アプリケーションを少し機能的にしようとするとすぐに、ProductsとCartの情報を組み合わせて何かしたくなるだろう。

Products : [
  {
    id,
    price,
  },
]    

Cart: [
  {
    productId,
    quantity,
  },
]

Cartに入っているある商品の数量と、Productsで定めている価格から小計をコンポーネントで出したい時などが、それにあたる。
このような場合どうすれば良いのか。

情報をネストさせる

CartのプロパティにProductsのエンティティそのものを含めてしまったらどうだろうか。
これなら、Cartだけの情報で、小計の計算が可能になる。

Cart: [
  {
    product: {
      id,
      price,
    },
    quantity,
  },
]

しかし、この方法では本来独立させておきたいProductとCartの情報をネストさせてしまうことで、構造が冗長かつProducts関連のアクションで対応すべきReducerの数が増えるので、場合によっては複雑になってしまうかもしれない。

参考:How to create nested reducers?

また、ProductId = 1の小計を出したいという状況では、その都度Cartの配列を走査して該当するものを抽出するというプログラムを書かなくてはならない。少々不便である。

情報をノーマライズする

上の例は、端的に言えば、

小計 = Products[id].price * Cart[productId].quantity とできたらそれで良い。情報をネストする必要もないのでシンプルさを保ったまま欲しい結果を手に入れることができる。これを実現するには、ProductsとCartのそれぞれを配列ではなくオブジェクトとして管理すれば良い。また、オブジェクトでは扱えない要素の順序関係については、別途要素のIDだけを保持する配列を用意する。

つまりこうなる。

擬似ストア
State {
  Products: {
    byId: {
      1: {
        id: 1,
        price: 20,
      },
    },
    Ids: [
      1,
    ]
  },

  Cart: {
    byProductId: {
      1: {
        productId: 1,
        quantitiy: 20,
      },
    },
    productIds: [
      1,
    ]
  }
}

小計が必要なコンテナコンポーネント内で
//商品IDが1の小計を返す関数
const getSubTotal = (state, id) => {
  const { Products, Cart } = state;
  return Products[id].price * Cart[productId].quantity;
}

参考: Redux nested reducers or normalize state?

API

ある状態を管理する reducer が実装すべきAPIは次のようなものになる。

reducer

xxxById :: (state = {}, action) => state

key に ID を持ち、value に実体を持つようなプロパティで構成されたオブジェクト。
通常は 配列 で保持するような情報を、ByID[ID]という形でDBのようにアクセスするため、オブジェクトで情報を保持している。

yyyIds :: (state = [], action) => state

あるオブジェクトの key のリスト。

selector

getXxx :: (parentState, ID) => {}

xxxの実体を取得するための selector 。これを使う側は、xxxを手に入れるための具体的な実装を知る必要がない。実装では、parentState.Xxxから目的のオブジェクトを取得する。APIとして公開する。

getYyyIds :: parentState => []

リストを取得するための selector 。実装では、parentState.YxxIdsを返却する。APIとして公開する。

親reducer

は階層構造を持てるので、任意のレベルでまとめ役としてのreducer(親reducerと呼ぶ)を持てる。親reducerでは、下位のreducerによって公開されたselectorを利用し、コンテナコンポーネント向けのAPIを公開する。このAPIは、UIインタラクションなどで渡されたID情報を活用しながら、下位のreducerで生成された各状態の関連を取りながら目的のデータを構築する。

54
46
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?