ReduxとRxjsと非同期処理
黒田 大介(Dycoon)です。 Application architectureの理解を深めておこうと思いReduxやRxJSで実験的なプログラムを書いてみました。 特にReduxでの非同期処理をどう書くべきかと言うのはよく話題になるのでそこに重点を置いて確認してみました。
実験プログラムの機能は以下のとおりです。
- Reduxのexampleのtodoの機能
- localStorageへの保存機能(最小限の非同期処理として)
これを以下の組み合わせで作りました。
- Redux+redux-observable
- RxJS(Reduxの役割をRxJSである程度再現)
デモはこちらです。
https://rmake.github.io/react-native-redux-experimental/
動きは大して変わらないのでRedux+redux-observableのもののみを配置しています。 ソースコードは以下から参照できます。
https://github.com/rmake/react-native-redux-experimental
https://github.com/rmake/react-native-rx-experimental
react-native-webを利用しておりReactNativeのコードでブラウザ上で動作できるようにしています。 AndroidやiOSアプリとしてもビルドできます1。
Reduxでの非同期処理
Reduxではactionを呼ぶとreducerにactionが渡されstateが変化します。 reducerの処理は同期処理でなければなりません。 そのため通信など非同期処理はreducer以外の場所で書きます。 また、その非同期処理では直接stateを変更してはいけません。 非同期処理の結果からactionをdispatchしてstateを変更するということをおこないます。 それではどこに非同期処理を書くべきかというと、いろいろ議論がされていて決定的な方法は無いらしいです2。
まずReduxのサンプルコードではredux-thunkを使用しています。 どのような書き方になるかというとactionに以下のように非同期処理を記述するようです。
const fetchPosts = reddit => dispatch => {
dispatch(requestPosts(reddit))
return fetch(`https://www.reddit.com/r/${reddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(reddit, json)))
}
この処理はactionsのフォルダで記述されています。 処理の起点なのでactionsに書くというのは筋が通った考え方のようにも思いますが分離した方が良い気もします。
別の場所に書く書き方としては redux-observableがあります。 redux-observableではmiddlewareの仕組みを使って作られておりactionに対応する処理をRxJSで記述します。 これについてはtodoアプリに適用してみた例を用意しました。
const loadTodos = (action$) => {
return action$.ofType("LOAD_TODOS").//delay(500).
mergeMap(action =>{
return AsyncStorage.getItem("todo").
then((data) => {
if (data) {
return loadTodosSuccess(JSON.parse(data));
}
else {
return loadTodosNoData();
}
}).
catch((error) => {
return loadTodosFailed(error);
});;
});
};
actionに対応する処理をepicsフォルダにまとめるという使い方をします。 actionsフォルダと別の場所に書くのも良さそうです。 redux-sagaもRxJSの部分が違う書き方ではありますが、actionに対応する処理を記述するという書き方ができます。
RxJSについて
redux-observableで触ってみたRxJSですがこれについて少し深くみてみます。
RxJSは非同期処理を簡潔かつ可読性高く書くことができる仕組みです3。
そのためときとしてRxJSはApplication architectureのメッセージ処理にも使われます。 Reduxよりもう少し単純な仕組みが欲しい、あるいはReduxとはちょっと違う仕組みの物を作りたいと思った時RxJSで自分の好みのものを作るのも良さそうです。 例としては以下のものがあります。
- MVI(Model-View-Intent)の実現に適用
- Reduxと同じような動きを実現してみた例
“Use RxJS with React](http://michalzalecki.com/use-rxjs-with-react/)”の記事の仕組みはちょっと面白く、 actionにそのまま非同期処理に関するOperatorを繋げることで非同期処理もそのままかけそうなのです。 元の記事では非同期処理の記述はおこなっていなかったので私の方で追加してみました。
actionの部分は以下のようにしました。
export default createActions([
...
{
name: "saveTodos",
message: (todos) => ({todos}),
},
...
]);
createActionsではactionごとにRx.Subjectを作ります。 middlewareの仕組みを実現するためにpayloadを取り出せるように書き換えています。
export const createAction = (name, messageCallback) => {
var subject$ = new Rx.Subject();
return {
send: (...args) => {
let message = messageCallback.apply(null, args);
message.type = name;
subject$.next(message);
},
handler: (callback) => {
return subject$.map(payload => {
return [
payload,
callback(payload),
]
});
},
name,
subject$,
};
};
actionを送信する場合はRx.Subjectから送り、その結果としてReducerが処理をおこないstateが変更されます。
actionに対応する非同期処理を書くのは単にactionのRx.Subjectにoperatorを追加してsubscribeすれば実現できます。
export const loadTodos = todoActions.loadTodos.handler(payload => {
return AsyncStorage.getItem("todo").
then((data) => {
if (data) {
return todoActions.loadTodosSuccess.send(JSON.parse(data));
}
else {
return todoActions.loadTodosNoData.send();
}
}).
catch((error) => {
return todoActions.loadTodosFailed.send(error);
});
}).subscribe();
この書き方ができるのはactionに対する処理を直接記述できている感じがして良いです。
実装する過程で、いくつかReduxと同じ動作にできていない点に気がついたので書いておきます。
- RxJS版ではmiddlewareの呼び出し頻度が少し高くなっています
- react-reduxでは関係するstateが変化したときだけrenderが呼ばれますが、RxJS版の方ではまだ実現できていません
- stateがimmutableならば前のstateと同一のインスタンスかどうか判定するだけで変化を検出できるので実現はそこまで難しくはないですが
まとめ
ReduxとReduxの非同期処理の記述方法と RxJSによる非同期処理を含めたアプリケーションアーキテクチャーの実装方法について調べてみました。 Reduxの仕組み自体は妥当だと思うので非同期処理などが他の仕組みを利用することになっていたとしてもReduxの仕組みを使うことにより得られるものはありそうです。 RxJSは応用範囲が広く自分好みのアプリケーションアーキテクチャーを作るのにも使えそうです。 今回はReduxを再実装してみましたが、MVIなど単純な仕組みを実現するとちょっとした実験をおこないたい場合の選択肢にもなりそうです。