名前も知らないSEでしょう

この木なんの木気になる樹

JavaScript

React関連の使い方を復習する/(6) reduxのStoreをimmutableにする

投稿日:

React関連の使い方を復習する/(1) create-react-app
React関連の使い方を復習する/(2) prop
React関連の使い方を復習する/(3) stateとイベントハンドラ
React関連の使い方を復習する/(4) prop-typesとreact-dnd
React関連の使い方を復習する/(5) react-redux、redux-actions

前回、reactで作成してきたアプリケーションにreact-reduxとredux-actionsを導入しました。
今回は、オブジェクトで管理していたstateを、immutable.jsを利用したオブジェクトに変更していきます。

Reactとimmutableを組み合わせたときのメリットは、ググれば色々と出てきますので詳細はそちらを参照してください。

ざっとメリットを挙げると、大きく以下3点でしょうか。

  • 参照透過性を保つことができる
  • (正しく使えば)パフォーマンスが向上する
  • モデルクラスを作ることができる

なお、参照透過性とは、以下を指します。

ある式が参照透過であるとは、その式をその式の値に置き換えてもプログラムの振る舞いが変わらない(言い換えれば、同じ入力に対して同じ作用と同じ出力とを持つプログラムになる)ことを言う。(wikipedia

中身を見ていく前に、immutable.jsを簡単に紹介します。
復習と言いつつ、実はimmutable.jsは今回初めて調べたのでなかなか学びが多かったです。

immutable.jsとは

immutable.jsは、Facebookが開発している、不変オブジェクトを扱うパッケージです。
扱うことができる主なクラスは色々とありますが、本記事では以下3つを紹介します。

  • Map
  • List
  • Record

基本的な挙動として、

  • 不変オブジェクトなので元のオブジェクトは変更されない
  • setやpushなどでプロパティを操作するメソッドを呼び出したら、操作後の新たなオブジェクトを返却する

となります。それを踏まえて、各クラスを見ていきます。

Map

JavaScriptのオブジェクトを扱うクラスです。

List

配列を扱うクラスです。
Listの中でオブジェクトを扱う場合、updateInなどを利用する場合はやはりfromJSでMap型に変換する必要があります。

Record

JavaScriptのオブジェクトを扱うのはMapと同様ですが、初期値や関数を定義することができます。いわゆる型定義ですね。

Reactと組み合わせる

それでは、Storeで管理するstateをimmutable.jsのオブジェクトに修正していきます。

Modelクラスを定義する

前回までに作成したstateと合わせて、アプリケーション全体の状態を管理するTodoModel、カードを表すCardModel、タスクを表すTaskModelの3つを作成します。

Card、Taskを新たに追加するときは、ひとまず簡易的にidはDate.now()で付与することにします。
連続で複数作成すると同じidになるでしょうけど、今回の本質ではないのでとりあえず置いておきます。

TodoReducer

続いて、TodoReducerを修正します。
前回、新たなインスタンスを作るためにObject.assignやArray.concatを使っていましたが、immutable.jsが常に新たなインスタンスを返却してくれるためこれが不要になります。

L57、TodoReducerが管理するStateの初期状態は、TodoModelのインスタンスに変更しています。各関数で行われる処理も、それぞれimmutable.jsの記法に合わせて修正していますね。

L50-L55でwithMutationsを使っていることが1つのポイントです。
オブジェクトを操作するメソッドを呼び出すと新たなインスタンスが返却されるので、メソッドチェーンで記載することもできます。しかし、普通にメソッドチェーンをするだけだとオブジェクトを操作するたびに新たなインスタンスが生成されるため、性能がよくありません。これらの処理をバッチで行うために"withMutations"を用いることで、それを改善することができます。

Appコンポーネント

ポイントはL36、L8でインポートしたImmutableWrapper#toJSでラップしています。これが何者かは後述します。
Storeで管理するStateはimmutable.jsを用いた型ですが、ReactのコンポーネントでPropsとして利用するときはプレーンなJavaScriptのオブジェクトに変換しないといけません。何も考えずに使おうとすると、以下のようにやりたくなります。

しかし、これは公式ドキュメントによると性能が悪いアンチパターンとのことです。
要は、毎回新しいインスタンスが返却されるから必ずshallow equality checkに失敗するので、値が変更されない場合でもreact-reduxによって再レンダリングされてしまうから性能が悪いよ、ということみたいです。

This is a particular issue if you use toJS() in a wrapped component’s mapStateToProps function, as React-Redux shallowly compares each value in the returned props object. For example, the value referenced by the todos prop returned from mapStateToProps below will always be a different object, and so will fail a shallow equality check.

(samle code)

When the shallow check fails, React-Redux will cause the component to re-render. Using toJS() in mapStateToProps in this way, therefore, will always cause the component to re-render, even if the value never changes, impacting heavily on performance.

これを回避するためのベストプラクティスとして、Higher Order Component(HOC)を使うとよいよ!ということが公式ドキュメントに書かれています。何やら難しそうな名称ですが、HOCとは他のコンポーネントをラップするコンポーネントのことです。公式を参考にしつつ作成したクラスが以下。

公式によると、immutable.jsのオブジェクトをHOCでプレーンなJavaScriptオブジェクトに変換することで、移植性を実現しつつパフォーマンスが低下することはないよ、ということみたいです。

By converting Immutable.JS objects to plain JavaScript values within a HOC, we achieve Dumb Component portability, but without the performance hits of using toJS() in the Smart Component.

知らないと普通にやりますね、これ。

index.js

最後に、redux-loggerが出力するログが、toJSで変換される前のimmutable.jsのオブジェクトになってしまいわかりづらいため、少々変換処理を追加します。該当部分のコードだけ。

combineReducersを使っている場合と使っていない場合で処理が異なりますが、redux-loggerのドキュメントに対処法が書いてあるので、これをes6の記法に合わせて記述してあげればOKです。

まとめ

以上でReactにimmutable.jsを導入することができました。
性能面で気にしなければいけないところが結構あって、思っていたよりずっと大変でしたけど、なかなか勉強になりました。使いこなせれば、より良いコードを書くことができそうですね。

現時点でのソースコードは、例によってGithubに置いておきます。

・・・日に日に記事の文字量が増えて辛くなってきた。冬休みの宿題と言いつつ冬休み中に終わる量ではない。
次回はカンバンのカードが入れ替えられるようになっていたりと急に機能が進化すると思います。
その後react-routerとテスト周りを書いて一段落としたい。

-JavaScript
-, ,

Copyright© この木なんの木気になる樹 , 2018 AllRights Reserved.