ReduxにおけるImmutableの概念についてまとめてみた


はじめに

ReactやReduxを触っていると、多くの人は、Immutable、mutableというワードを耳にするのではないでしょうか。もちろんこれらは他の言語やフレームワークを使用するにあたっても重要とは思いますが、React、Reduxで開発するにあたってはとりわけ重要な概念です。実際、Reduxのstyleガイドで挙げられているReduxによるコーディングの原則の一番初めにデカデカと書かれているのが、mutableにデータを変更してはならないという指摘です。それくらい重要な概念であるImmutableですが、ちょっと理解が難しく、また理解しなくても一通り開発が進めらてしまうため(もちろんそのうち壁にぶち当たりますが)、言われてもあまりピンとこないのではないでしょうか?(筆者自身もそうでした)。この記事では、その概念について、利点や、実際のコーディング例も交えながらわかりやすく解説していきたいと思います。これを最後まで読めば、あなたもImmutableについての理解が深まると思います。

Immutableコーディングの利点

Immutableなコーディングには複数のメリットがあります。
1. シンプルで可読性の高いコーディングが可能になる
2. デバックしやすい
3. React等のDOM動作のフレームワークが正確に動作する

なぜReduxにおいてImmutableなコーディングが必要なのか

  • Redux、React-Reduxはともにshallow equality checkingを採用しています。ReduxのcombineReducersでは、そのcombineReducersが呼び出したreducerの変化をshallow checkingしている。React-Reduxのconnectメソッドでは、RootStateの変化をshallow checkingして、コンポーネントが再renderされるかを判別しています。
  • Immutableなコーディングでは、より安全にデータを扱うことができるようになります。
  • 副作用のない関数によってreducerが定義されることにより、デバックの際、別個のstate間を正しく追跡することが可能になります。

そもそもshallow equality checkingとは?deep equality checkingとの違い

shallow equality checking => 2つの異なる変数が同じ参照元(データの保存先のメモリの番地)かどうかをみているのみです。そのため、実際のデータの中身のみが変化しても、そのデータの参照元が同じである場合、変更が検知されません。
deep equality checking => 2つの異なる変数の実際のデータの値が同一のものであるかどうかをみています。

Reduxでは、そのパフォーマンスの向上の理由上からshallow equality checkingが採用されています。

具体例1

Reduxのstateを変更する際。
悪いコード例)

const exampleReducer = reducerWithInitailState(initialState)
    .case(actions.exampleAction, (state, payload) => (
        state.push(payload)
    ))

良いコード例)

const exampleReducer = reducerWithInitailState(initialState)
    .case(actions.exampleAction, (state, payload) => (
        state.concat(payload)
    ))

上記の2つの例の違いは、元のstate(ここでは配列を想定)にどのように引数を追加しているかという点です。1つ目の例で使われているpushメソッドは、配列に対して要素を追加する際、元の配列を直接書き換えて、変更します。そのため、その配列の参照元(データの保存先のメモリの番地)が変わることはありません。
一方で、2つ目の例にあるconcatメソッドは、元の配列に影響を与えず、結合された新たなオブジェクトを生み出します。そのため、元の配列を複製した上で新たな配列(結合された新しい配列)を生成するため、元のstateの配列と新しい配列ではその参照元が異なります。

具体例2

悪いコード例)

const initialstate = {
    name: "Rui",
    age: 22,
    address: "多摩"
}
const exampleReducer = reducerWithInitialState(initialState)
    .case(action.exampleAction, (state, payload) => (
      state.address = "六本木"
    ))

良いコード例)

const initialstate = {
    name: "Rui",
    age: 22,
    address: "多摩"
}
const exampleReducer = reducerWithInitialState(initialState)
    .case(action.exampleAction, (state, payload) => (
       ...state,
       address: "六本木"
    ))

悪いコード例では、stateのaddressを直接書き換えていますが、良いコード例では、stateを一度複製し、複製したstateに対して変更を加えているため変更前のstateが書き換わっていません。

おわりに

ReduxおよびReact-Reduxにおいては、shallow equality checkingが採用されているためstate等のデータを直接書き換えるようなImmutableでないコーディングを行ってしまうと、その変更が検知されず、stateの変更があったにも関わらず、componentが正しく再renderされないなどの事態が生じる恐れがあります。また、はじめの方でも書いたように、Immutableなコーディングはデバックの際にもたいへん有効ですし、コード全体の可読性も上がるため、チーム開発の際にもとてもメリットがあります。気にかけていないと、ついうっかりmutableなコーディングをしてしまいがちなので、ぜひ注意してくださいね! Reduxの公式FAQ(下にもリンクを貼っておきます)はかなり詳細にこの概念について書かれていて、とても勉強ななるので、ぜひ一読してみてください。

参考にしたサイト