Reduxの不満点は、機能を実装するのに必要なボイラープレートのコード量の多さです。解決する方法にMobXがあります。MobXはReduxと同様の機能を持ちますが、記述するコードの量が少なくて済むライブラリーです。
MobXの初心者は、MobXの開発者が書いた入門記事がおすすめです。実践的な経験にはチュートリアルに取り組むのもよいでしょう。
この記事では、JavaScriptの開発者が、2つのステート管理法のどちらがプロジェクトに合っているか、決める手助けをします。サンプルを作るために、CRUD Redux projectをMobXに移植しました。
MobXの良い点と悪い点を解説し、2つのバージョンで実際にコードを書いて、違いを紹介します。
本記事で扱うプロジェクトのコードは、GitHubにあります。
ReduxとMobXの共通点
ReduxとMobXの共通点は以下の通りです。
- オープンソースライブラリーである
- クライアント側のステート管理ができる
- redux-devtools-extensionを使ったタイムトラベルデバッグをサポートしている
- 特定のフレームワークに縛られない
- React/Reactネイティブフレームワークを広範にサポートする
MobXを使う4つの理由
ReduxとMobXの主な違いは以下の通りです。
1.覚えやすく使いやすい
30分あれば、初心者でもMobXの使い方を覚えられます。基礎以上のことを学ぶ必要がないのです。Reduxも基礎は簡単ですが、複雑なアプリケーションを作るには以下のことを覚えなければいけません。
- redux-thunkで非同期動作を処理する
- redux-sagaを使ってコードを簡素化する
- 計算値を取り扱うセレクタを定義する、など
MobXは、これらを「魔法のように」処理します。追加のライブラリーは必要ないのです。
2.書くコード量が減る
Reduxで機能を実装するには、レデューサー、アクション、コンテナ、コンポーネントのコードを含み、少なくとも4つのアーティファクトを更新します。取り組んでいるプロジェクトが小さいほど、面倒な作業です。MobXではストアとビューコンポーネント、2つのアーティファクトを更新すればいいのです。
3.オブジェクト指向プログラミングの完全サポート
オブジェクト指向のコードを書くのが好きなら、MobXはステート管理ロジックの実装にOOPが使えるので好都合でしょう。@observableや@observerなどのデコレータを使い、素のJavaScriptコンポーネントやストアを簡単にリアクティブにできます。関数型プログラミングのほうが好きでもサポートされているので問題はありません。Reduxは、関数型プログラミングの原則を強く指向していますが、クラスベースのアプローチをしたければ、redux-connect-decoratorが使えます。
4.ネストデータの取り扱いが簡単
JavaScriptアプリケーションは、リレーショナルデータやネストデータを取り扱います。Reduxストア内で使うには、正規化が必要です。次に、正規化されたデータの参照を追跡し管理するために何行かコードを書きます。
MobXは、データを正規化せずに格納することを推奨しています。MobXは、リレーションを追跡し、変化があれば、自動的にリレンダーします。データの格納に、ドメインオブジェクトを使ってほかのストアで定義された別のドメインオブジェクトを直接参照します。さらに、(@)computed decoratorsとmodifiers for observablesで、データに関する複雑な課題を簡単に解決します。
MobXを使わない3つの理由
1.自由度がありすぎる
Reduxはステートコードの書き方に厳しいガイドラインを提供しているフレームワークです。簡単にテストを書いて、メンテナンス可能なコードを開発できないのです。MobXはライブラリーで、実装にルールがありません。ショートカットをして、簡単に修正ができるので、メンテナンスできないコードになる危険性があります。
2.デバッグが難しい
MobXの内部コードは、「魔法のように」多くのロジックを処理し、アプリケーションをリアクティブにします。ストアとコンポーネントの間にデータが通過する際、見えない領域があるため、問題が起こったときにデバッグが難しくなります。@actionsを使わずに、コンポーネントでステートを直接変更すると、バグの発生源を突き止めるのに苦労します。
3.MobXよりも良い選択肢があるかもしれない
ソフトウエアの開発は、常に新しいトレンドが現れます。数年で使っているソフトウエアの技術が使われなくなることもあります。Relay/Apollo & GraphQLやAlt.js、JumpsuitなどReduxとMobXともにいくつかの競合が存在し、一番人気になるものが出てくる可能性もあるのです。本当に自分にあうものを知りたいなら、すべて試してみなければなりません。
コードの比較:Redux vs MobX
理屈はここまでにして、コードを見てましょう。ブートストラップの表現を比較します。
ブートストラップ
Redux版
Reduxは、ストアを定義して、Provider経由でAppに渡します。また、redux-thunkとredux-promise-middlewareを定義して、非同期関数を処理をします。redux-devtools-extensionなら、ストアをタイムトラベルモードでデバッグできます。
// src/store.js
import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from "./reducers";
const middleware = composeWithDevTools(applyMiddleware(promise(), thunk));
export default createStore(rootReducer, middleware);
-------------------------------------------------------------------------------
// src/index.js
....
ReactDOM.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>,
document.getElementById('root')
);
MobX版
MobXは、ストアを複数設定します。例では、ストアを1つ使い、コレクション「allStores」に置きます。ストアのコレクションをAppに渡すためProviderを使います。
MobXは非同期動作のための外部ライブラリーが不要なので、コード行数が少なくて済みます。ただし、redux-devtools-extensionデバッグツールにつなぐのにmobx-remotedevが必要です。
// src/stores/index.js
import remotedev from 'mobx-remotedev';
import Store from './store';
const contactConfig = {
name:'Contact Store',
global: true,
onlyActions:true,
filters: {
whitelist: /fetch|update|create|Event|entity|entities|handleErrors/
}
};
const contactStore = new Store('api/contacts');
const allStores = {
contactStore: remotedev(contactStore, contactConfig)
};
export default allStores;
-------------------------------------------------------------------------------
// src/index.js
...
ReactDOM.render(
<BrowserRouter>
<Provider stores={allStores}>
<App />
</Provider>
</BrowserRouter>,
document.getElementById('root')
);
コードの量は、2つのバージョンでほぼ同じですが、MobXはインポート文が少なくなっています。
Propsの挿入
Redux版
Reduxでは、ステートとアクションはreact-reduxのconnect()関数でpropsに渡します。
// src/pages/contact-form-page.js
...
// accessing props
<ContactForm
contact={this.props.contact}
loading={this.props.loading}
onSubmit={this.submit}
/>
...
// function for injecting state into props
function mapStateToProps(state) {
return {
contact: state.contactStore.contact,
errors: state.contactStore.errors
}
}
// injecting both state and actions into props
export default connect(mapStateToProps, { newContact,
saveContact,
fetchContact,
updateContact
})(ContactFormPage);
MobX版
MobXは、storesコレクションを挿入するだけです。コンテナあるいはコンポーネントクラスの最初で@injectを記述すると、propsの中でstoresが使えるため、特定のストアにアクセスして、子コンポーネントに渡せます。ステートもアクションもstoreオブジェクトのプロパティ経由でアクセスするので、Reduxのように別々に渡す必要はありません。
// src/pages/contact-form-page.js
...
@inject("stores") @observer // injecting store into props
class ContactFormPage extends Component {
...
// accessing store via props
const { contactStore:store } = this.props.stores;
return (
<ContactForm
store={store}
form={this.form}
contact={store.entity}
/>
)
...
}
MobXが読みやすいように見えますが、Reduxはコードを簡略化するのに、redux-connect-decoratorsを使えます。どちらに軍配が上がるかは分かりません。
ストア、アクション、レデューサを定義する
記事をスリムに保つため、アクションのサンプルコードを1つ示します。
Redux版
Reduxは、アクションとレデューサを定義します。
// src/actions/contact-actions.js
...
export function fetchContacts(){
return dispatch => {
dispatch({
type: 'FETCH_CONTACTS',
payload: client.get(url)
})
}
}
...
// src/reducers/contact-reducer
...
switch (action.type) {
case 'FETCH_CONTACTS_FULFILLED': {
return {
...state,
contacts: action.payload.data.data || action.payload.data,
loading: false,
errors: {}
}
}
case 'FETCH_CONTACTS_PENDING': {
return {
...state,
loading: true,
errors: {}
}
}
case 'FETCH_CONTACTS_REJECTED': {
return {
...state,
loading: false,
errors: { global: action.payload.message }
}
}
}
...
MobX版
MobXは、アクションとレデューサのロジックを1つのクラスで実行します。responseを受けたあと、もう1つのアクションentities fetchedを呼ぶ非同期アクションを定義しました。
MobXはOOPスタイルを使うので、定義したStoreクラスは分解されて、クラスコンストラクタで複数のストアを簡単に生成させます。以下のコードは、特定のドメインのストアにひもづけられていないベースコードです。
// src/stores/store.js
...
@action
fetchAll = async() => {
this.loading = true;
this.errors = {};
try {
const response = await this.service.find({})
runInAction('entities fetched', () => {
this.entities = response.data;
this.loading = false;
});
} catch(err) {
this.handleErrors(err);
}
}
...
信じられないかもしれませんが、2つのバージョンで定義したロジックは同じタスクを実行します。
- UIローディングステートを更新する
- データを非同期で取る
- 例外処理を検知し、ステートを更新する
Reduxでは33行のコードを使いましたが、MobXではたった14行で同じ結果を得られます。MobX版の主な利点は、ほとんど、あるいはまったく変更なしに、ドメインストアクラスでベースコードを再利用できることです。アプリケーションをより短時間で作れるのです。
そのほかの違い
フォームの作成に、Reduxはredux-formを、MobXはmobx-react-formを使いました。どちらのライブラリーも成熟していて、フォームロジックを簡単に取り扱えます。個人的には、プラグインを使ってフィールドが検証できるのでmobx-react-formが好きです。redux-formでは、検証用のコードを書くか、検証をする検証パッケージをインポートします。
MobXの小さな欠点は、オブザーバブルオブジェクト中の関数に、JavaScriptのオブジェクトではなく、直接アクセスできないものがあることです。幸い、関数toJS()を用意していて、オブザーバブルオブジェクトを素のJavaScriptオブジェクトに変換します。
最後に
MobXのコードベースが簡潔だと分かりましたか。OOPスタイルとよい開発プラクティスに従えば、短時間でアプリケーションを作成できます。主な欠点は、下手な書き方でもコードが書けてしまうため、メンテナンスできないことです。
Reduxは使う人が多く、大規模で複雑なプロジェクトに適しています。開発者が全員、テストとメンテナンスが容易なコードを書くように設計された厳格なフレームワークです。小さなプロジェクトには向いていません。
Mobxは大きなプロジェクトが苦手ですが、グッドプラクティスに従えば、大きなプロジェクトも作れます。アインシュタインの言葉を借りると、「ものごとはできる限りシンプルにすべきだ。しかし、シンプルすぎてもいけない」のです。
MobXに乗り換えるか、Reduxにこだわるかの判断に十分な情報が提供できていたら幸いです。結局、どのようなタイプのプロジェクトに使うか、どのようなリソースが使えるかによるのです。
本記事はDominic MyersとVildan Softicが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Redux vs MobX: Which Is Best for Your Project?)
[翻訳:関 宏也/編集:Livit]