React/Redux 使ってみての勘所
前回のエントリでは、React/Redux/ES6のざっくりとした感想をまとめました。
今回はその続きということで、React/Redux利用時において、
- こうすればよかった!!
- こうすべきじゃなかった・・
といったところをまとめてみたいと思います。
Immutable.js は使ったほうが幸せになれた
https://facebook.github.io/immutable-js/
javascriptで不変データ構造を提供してくれるライブラリ。(by facebook) これによって解消される問題が多数。
構造がネストした場合の更新が簡単に
「そもそもネストしないような構造にしろよ」という話なのですが、 大人の事情でネストせざるを得ないときもあります。
ネストしたstoreの場合、action経由で深い階層の値を更新したい場合に結構な手間になります。
たとえば user[0].products[2].item.name
みたいになってしまったケース。
Object.assignやlodash#mergeなどを利用するのも手だと思いますが、 動作をきちんと理解せずに利用すると、
- 同オブジェクト内の存在しないキーが落ちる
- undefinedの値が落ちる(落ちない)
といった状態になり、ハマってしまうことがありました。 気をつければいいのですが、「気をつけているコード」で溢れかえるとかえって読みにくくなったり。
これがImmutable.jsを利用していると
state.setIn(["user", 0, "products", 2, "item", "name"], "yeah");
でstate自体に影響を与えることなく、新しいstateを得ることができます。
記述が統一できる
reducerで新しいstateを生成する際には、引数として受け取ったstateを書き換えず、新しいstateを生成する必要があります。
そのため、state生成時には操作時に注意することが出てきます。
- 配列の要素を変更する場合
- Array#concat などで新しい配列を作って差し替える
- オブジェクトの生成方法が1つではない
- Object#assign
- lodashのメソッド(assign/merge/extend/default...)
- 普通に自力で作成
これらで対応することそのものが問題ではありませんが、「ネストした場合の更新」の欄でも触れた通り、方法によってundefined状態のフィールドの取り扱いが違ったりするため、複数人での開発時には記法を統一するほうがベターだと思います。
「reducerでのstate操作はImmutable.jsで」としておけばそれだけである程度は操作が統一できるため、心理的負担も少し下がります。
比較が簡単に
Reactでコードを書いていくと、どこかでパフォーマンスチューニングのために、shouldComponentUpdate
によるレンダリング抑制を書く機会が登場します。
このとき、とりあえず this.props
と nextProps
を比較して差異がなければレンダリングしない(return false)とするケースが多いのですが、Immutable.jsを利用していれば、===
を利用して容易に比較することが可能となります。
利用しない場合には自力で比較するか、lodash#isEqualなどを利用することになりますが、細かいところでカスタマイズが必要になるケースが多いです。
Reactの公式docにもImmutable.jsに関する記載はありますね。
構造をコード上に定義しておける
redux(flux)を利用した場合、アプリケーションの状態を表すstoreは一箇所で管理されますが、初期状態とすべきstore状態をどこに定義するか?という問題が発生します。
reducerファイル内に直接定義してしまっても大丈夫ですが、規模が大きくなってくるとカオスになりがちです。というかカオスになりました。
そこで、Immutable.jsを利用してデフォルトのstore定義のみを定義しておくことで、store全体がどのような構造かすぐ解るようになり、見通しが良くなりました。
また、reducerを分割管理している場合などは、ベースとなるstoreを定義しておき、そこからImmutable.jsのmergeを利用して拡張することで、継承っぽくstore定義していくこともできます。(お作法的にどうなのかは謎)
export const User = Immutable.fromJS({ id: null, name: "", email: "" }); export const AdminUser = User.merge({ tel: "", address: "", permission: false });
function user(state = User, action) { switch (action.type) { case 'USER_UPDATE': return /* update */; } return state; } function admin(state = Admin, action) { switch (action.type) { case 'ADMIN_UPDATE': return /* update */; } return state; }
ただ、Immutable.jsはファイルサイズが大きいのがネックとなるケースもあるようです。
ご利用は計画的に。
connectはひかえめに
connect記述により、任意のstoreをsubscribeすることができます。
これを利用すると、Reactで陥りがちなpropsのバケツリレーをぶった切ることが可能となります。
・・が、実際にはあまりこれはオススメできません。 最初はガンガンconnectを利用してコーディングしてしまっていたのですが、後になって色々と問題が表面化してきました。
再利用しにくくなる
connectしてstoreをsubscribeするということは、そのstoreに依存していると言っても間違いではないと思います。 一概に全てというわけではありませんが、大半のケースでは、connectを利用したcontainerは1つの用途に限定されてしまい、「あー、connectsしてるから使えないじゃーん!」というケースが発生して悲しい思いをしました。
パフォーマンスに影響が出てくる
通常のReactコンポーネントであれば、親要素で shouldComponentUpdate
によるレンダリング抑制を行った場合、子の要素も自動的にレンダリングが抑制されます。
とても素直な考えだと思いますが、ここで子がconnectしてしまっていると、親からのprops伝搬とは別に、storeの変更時にもレンダリングが発生することになります。
つまり、子自身の中で shouldComponentUpdate
による抑制を行う必要が発生します。別にそれでもいいじゃん、という発想もありますが、数が増えてくると shouldComponentUpdate
そのものが大量になってしまったり、パフォーマンス低下時に原因を追求するのが難しくなっていきます。
「親で shouldComponentUpdate ちゃんと動いてんのになんでこんなに描画重くなるんだ・・・」というときにこれが原因でした。
というわけで
などなどの理由から、基本的には、connectするのは最上位階層のみとし、子以下には素直にpropsでバケツリレーとするほうが、最終的には可読性・保守性ともに望ましい形のコードになりました。 SPAの場合は、ルーティング単位でrecducer分割してconnectするとちょうど良い感じですが、そのあたりは作成するアプリケーションに応じて差異があるかと思います。
いずれにしても、乱用しないほうが望ましいかも。
Storeはできるだけフラットな構造に
Immutable.jsの欄で「ネスト時の更新が簡単に!」とか書いてますが、そもそもの話としてはStoreはできるだけネストさせないほうが望ましいです。
理由は
- ネストしていると、undefined/null を意識する手間が増える
- そもそも更新がめんどうになる
- PureRenderMixinが使えるようになる
などです。
normalizeするのも1つの方法ですね。
思い返すと他にもたくさんポイントとなる箇所はありましたが、また同構成を利用する機会があれば、とりあえずは上記は最初から抑えた状態で着手したいな〜という感じです。
ただ、やはりもう少し軽いもの(実装量的に)があれば嬉しいな〜
めまぐるしく変化し続けている世界ですし、2016年にもまた何か大きい変化があったりするのかな。