はじめに
この記事はDMM.com #1 Advent Calendar 2017 の12/13の記事です。
Immutable.JSを使って、React+Redux製WebアプリのReducerをすっきりさせた方法と結果、及び注意点についてまとめました。
目的
Immutable.JSとは、Facebookが開発した、不変であるデータ構造を扱うためのJavaScriptライブラリです。
開発中だったReact+Reduxアプリでは、開発を進めれば進めるほど、ActionとReducerが肥大化し、改修がしづらくなっていました。
(Reducer内にロジックが含まれていたり、冗長な...state
を書く必要があったり…etc。)
今回は、Immutable.JSを使って、Reducerの肥大化を防ぎ、保守性・改修性を上げることを目的とします。
方法
今回は一般的なReact+Reduxアプリのフローに、Modelという概念を追加します。
Modelでは、actionに応じた処理を記述し、immutableなオブジェクトをReducerに返します。
また、今回は Using Immutable.JS with Redux に出来るだけ沿った記述を行いました。
Immutable.JSを使う際に注意すること
書き方が独特
覚えてしまえば楽なのですが、Immutable.JS特有の書式を覚える必要があります。
そのため普通のReact+Reduxと比べると学習コストが少し上がります。
パフォーマンスを重要視した記述について
パフォーマンスを重要視するなら以下の記述に注意すべし。
その1
// これでもうごく
state.set('loading', false).set('user', user)
// さいてき
state.withMutations(s => s.set('loading', false).set('user', user))
Immutable.JS は、内部で何度も再編成を行いデータを構築するため、
withMutations
を使って再編成が1回で済むような記述をすることが望ましいようです。
その2
// これでもうごく
const mapStateToProps = (state) => ({
loans: state.get('loans').toJS(),
})
// さいてき
const mapStateToProps = (state) => ({
loans: state.get('loans'),
})
mapStateToProps
でいちいち toJS()
をすると、再編成する回数が増え、パフォーマンスがやや下がるようです。。
オブジェクトの構成について
Immutable.JS でラップされたオブジェクトは、普通のオブジェクトと中身や構成がだいぶ異なるので注意。
// 通常のオブジェクト
{
type: 'test',
message: 'fuga',
}
// Immutableなオブジェクト
Hoge {
values: List {
size: 2,
__altered: true,
__hash: undefined,
__ownerID: undefined,
_level: 5,
_origin: 0,
_root: null,
_tail: VNode {
array: ['test', 'fuga'],
ownerID: ownerID {},
},
}
}
値を取り出して使いたかったら toJS()
する必要があります。
ただし mapStateToProps()
内で toJS()
をするとパフォーマンスが下がります。気にする場合はHigher Order Component を介して toJS()
を行うことで回避できます。
結果
コードはどうなったか
例えば、Reducerの初期値が
{
input: {
name: 'hoge',
item: {
text: 'hello, Immutable.JS!',
type: 'fuga',
},
},
data: {
name: 'test',
boxes: [
{
name: 'hoge',
items: [],
},
],
},
};
であるとき、ADD_ITEM_IN_BOX
というActionが発行された場合はどうなるか?
before
※redux-actions
のhandleActions()
を使用した記述をしています
ADD_ITEM_IN_BOX: (state, action) => {
const { index, item } = action.payload;
const box = state.data.boxes[index];
const items = [...box.items, item];
const newBox = {
...box,
items,
};
const newBoxes = [...state.data.boxes];
newBoxes[index] = newBox;
return {
...state,
data: {
...state.data,
boxes: newBoxes,
},
};
},
after
ADD_ITEM_IN_BOX: (state, action) => {
return state.addItemInBox(action.payload.index, action.payload.item);
}
addItemInBox(index, item) {
const items = this.get('data').get('boxes').get(index).get('items');
return this.setIn(['data', 'boxes', index, 'items'], items.push(fromJS(item)));
}
なんとコードの行数が半分以下になりました。
保守性はどうなったか
platoという性能可視化ツールで可視化された解析結果を見ると、行数はアプリ全体でおよそ10000行ありましたが、そのうちの約1000行、全体の1割のコード削減に成功しました。
またReducerファイルの保守性はそれぞれ5~20%上がっており、下がっていたものはありませんでした。
Modelファイルの保守性はどれも70~90%台と高いスコアが出ていました。
これらの結果から、肥大化し改修がしづらくなっていたReducerは、Immutable.JSを適用することで改修がしやすくなり、読みやすくなった、といえるでしょう。
反省・感想・今後の展望
Modelいらない説
今回は、異なるactionで処理する内容が同じであることが多かったためModelに処理を委ねていい感じにすることができましたが、
例えば1つのactionにつき処理が全て異なる、という場合は、Modelを作らずとも、Reducer内に記述を直書きしてもよいなーと思いました。(ググるとこちらの例の方が多いです)
超時間かかった
また、今回は開発の途中でImmutable.JSを導入したせいか、完全移行まで物凄く時間がかかってしまいました(Reducer10個、Action100個近くあるアプリで2週間くらい)。
もし取り入れるのであれば、開発の一番最初から取り入れたほうが吉です。
Action Creatorsの圧縮
実はAction Creatorsについても、もっと短く出来るのではと思い挑戦していましたが、redux-saga
等のmiddlewareとの兼ね合いで実現できませんでした。アプリの構成にもよりますが、これを実現するのは難しいと思いました。
まとめ
Immutable.JSをReduxに適用することで、コードの改修がしやすくなることが分かりました。
ただ、独特な書式だったり、書き方によってはパフォーマンスが落ちたりするので、扱う際は注意が必要です。
具体的なModelの作り方については今回は省きましたが、別の記事で紹介する予定です。
参考記事
React使い必見! Immutable.jsでReactはもっと良くなる
Immutable.jsでreact+redux環境が楽になりそうな話