2017/12/29更新: ReduxとMobXの選定観点 も併せて見ていただければ幸いです
front-end-handbook-2017 に名前が挙がっていた MobX に興味が湧き調べてみました。その結果、掲題のとおりの記事を書きたくなるまでに至ったので、個人的にReduxより優れていると感じた点を挙げたいと思います。
- 記述量が圧倒的に減る
- Store概念のわかり易さ
バケツリレーが実質不要になる- injectを活用するとjsxが純粋になる
- デコレーター層の存在
記述量が圧倒的に減る
一つの双方向な値をコンポーネントに表示するために、Reduxでは以下の作業が必要でした。
- Reducer に initialState として値を追加
- Reducer で Object.assign した新しい State を生成
- ActionType を追加
- ActionCreator を追加
- ActionDispatch する コンポーネントで ActionCreator をバインディング
- 表示を受けたいコンポーネントで、Reducer から props展開
- 必要があれば、表示を受けたいコンポーネントの親が props をバケツリレー
いくら慣れてもこの非効率感は否めません。
一方MobXでは、以下の様になります。
- Store に値を追加
- Store の値を操作する関数を追加
- Store を操作したいコンポーネントで 関数を実行
- Store を参照したいコンポーネントで observer を宣言
単純にdiffが削減されることが分かります。
とてもシンプルです。
Store概念のわかり易さ
ReduxにおけるStoreは唯一無二で、Developerには隠蔽された状態で提供されます。そして Reducer を経由することで、Storeの値を参照できます。また、ActionType・ActionCreator を経由して Storeの値を変更できます。初学者が「Reduxとっつきにくい」と感じるのはここにあると思っています。
Mobxではそうではありません。
コンポーネントが参照したい Store を宣言し、参照します。
Store の値を変更したければ、Storeに依頼します。
とてもシンプルです。
export default class SomeStore {
@observable count // 追加した値
increment = () => this.count++ // 追加した関数
}
const SomeComponent = ({ someStore }) => {
return (
<div>
<p>{ someStore.count }</p>
<button onClick={ someStore.increment }> +1 </button>
</div>
)
})
// 関心のある Store を inject(注入) し、 observe(購読)する
export default inject('someStore')(observer(SomeComponent))
バケツリレーが実質不要になる
Reduxでは、コンポーネントがStoreの値を参照するため、Containerのpropsを、必要なコンポーネントまでバケツリレーで渡さなければいけません。中間コンポーネントは必要の無い props を受け流すだけの記述が必要になります。
Mobxではこの無駄な作業がありません。
コンポーネントが参照したい Store を宣言し、必要な値を、必要な形式で注入することが出来ます。バケツリレーが不要になったことで、コンポーネントをより純粋に保つことができます。
以下で詳しく説明していきます。
【お詫びと訂正】
本節は下記実装を採用した際の特記事項です。mobx-react v4.1.2 の injectは context参照を binding しています。React v15.4.2 においては、context参照は実験的で、将来的に利用できなくなる可能性があります。採用する際は、それらの動向を気にかけるようにしましょう。
injectを活用するとjsxが純粋になる
Reduxでは、Container から渡された props の必要な参照を render前に展開し、jsxに注入します。MobXでも同様の手順が必要ですが、mobx-react の Provider と inject を使うことで見通しの良いコンポーネントを定義することができます。mobx-react v4 から追加された injectの mapperFunction
がそれです。
- propsを抽象化できる
- propTypesの見通しが良くなる
- mappingを変更すれば、異なる observerComponent が export 出来る
- よりテスタブルになる
inject の mapperFunction により抽象化されたことで、再利用性が高くなったコンポーネントの例を挙げてみます。
import Mobx, { inject } from 'mobx-react'
export const Todos = ({ todos, count, add, remove }) => {
return (
<div className="todos">
{ todos.map((todo, i) => <Todo key={ i } index={ i } />) }
<p className="todos-count">{ count }</p>
<button onClick={ add }>addTodo</button>
<button onClick={ remove }>removeTodo</button>
</div>
)
}
export const OpenedTodos inject(s => ({
todos: s.todoStore.openedTodos,
count: s.todoStore.openedCount,
add: s.todoStore.addTodo,
remove: s.todoStore.removeTodo,
}))(Todos)
export const ClosedTodos inject(s => ({
todos: s.todoStore.closedTodos,
count: s.todoStore.closedCount,
add: s.todoStore.addTodo,
remove: s.todoStore.removeTodo,
}))(Todos)
Todos.propTypes = {
todos: Mobx.PropTypes.observableArray,
count: React.PropTypes.number,
add: React.PropTypes.func,
remove: React.PropTypes.func
}
2017/02/27[修正]:mapperFunctionを利用する場合は observer decorate を取り除く必要があるので修正しました
N.B. note that in this specific case neither NameDisplayer or UserNameDisplayer doesn't need to be decorated with observer, since the observable dereferencing is done in the mapper function
2017/03/09[修正]:injectは上記の通りcontext参照をbindingしているため、安定的な選択肢とは言えないため、内容を修正しました
デコレーター層の存在
ここでいうデコレーター層とは @
の es6 decorator ではありません。
Ruby on rails の DraperDecorator の様なもので、Store の @computed
を指しています。
アプリケーションを構築するうえで、否応でもjsx中に表示に関するロジックが増えてきます。
そこを吸収してくれるのが @computed
です。
@observable
な値が変更されたとき、@computed
な値も変更される挙動から、
自分のなかで、DraperDecoratorの様な利用が出来るなと感じました。
Reduxでも等価の処理をすることは可能ですが、
Reducerの見通しがどんどん悪くなってしまうことは想像し易いです。
総括
Reduxを導入する利点に DevTool や ドキュメントが充実している点があります。
MobXはまだまだこれから、という感じです。
自分の様に、redux-saga を使っていた場合は、非同期の扱いや、他の副作用は?という点も
考慮しないといけません。この点については、別の機会に書きたいと思っています。
2017/02/28[修正]:続編を公開しました >> 次のReact状態管理 MobXのStore考察