読者です 読者をやめる 読者になる 読者になる

React/Redux 使ってみての勘所

JavaScript React プログラミング Redux

前回のエントリでは、React/Redux/ES6のざっくりとした感想をまとめました。

mugi1.hateblo.jp

今回はその続きということで、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.propsnextProps を比較して差異がなければレンダリングしない(return false)とするケースが多いのですが、Immutable.jsを利用していれば、=== を利用して容易に比較することが可能となります。

利用しない場合には自力で比較するか、lodash#isEqualなどを利用することになりますが、細かいところでカスタマイズが必要になるケースが多いです。

Reactの公式docにもImmutable.jsに関する記載はありますね。

facebook.github.io

構造をコード上に定義しておける

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つの方法ですね。

github.com



思い返すと他にもたくさんポイントとなる箇所はありましたが、また同構成を利用する機会があれば、とりあえずは上記は最初から抑えた状態で着手したいな〜という感じです。

ただ、やはりもう少し軽いもの(実装量的に)があれば嬉しいな〜

めまぐるしく変化し続けている世界ですし、2016年にもまた何か大きい変化があったりするのかな。