この記事では、 TypeScript FSA で非同期処理するときの知見を忘備録として載せます。
TypeScript FSA 自体は他の方の投稿のほうが詳しく紹介されているので、この記事ではニッチな部分をメインに取り上げて載せています。
サンプルプロジェクトはこちら。
目次
- TypeScript FSA 使い方 3.0.0-beta-2
- Redux Thunk と一緒に 2.3.0
- Redux Saga と一緒に 1.0.0-beta.1
- redux-observable と一緒に 1.0.0
- 最後に
TypeScript FSA 使い方
TypeScript FSA は、 “Flux Standard Action” に TypeScript で型を付けることで、 Action 作成関数と Reducer の実装を型安全に行えるようにするライブラリです。
Action の Dispatch 時の引数と Reducer での payload
の型をつけられるので強力です。後者は isType
という User-Defined Type Guards により実現しています。
TypeScript FSA の非同期処理の便利なツールとしては、( const actionCreator = actionCreatorFactory()
と Action 作成関数ジェネレータを作成したとして) actionCreator.async<Params, Result, Error>()
というメソッドがあり、 _STARTED
_DONE
_FAILED
というポストフィックスがついた Action を一気に生成できます。
3 種類しかありませんが、簡単な処理や、単純な Action で済む場合なら十分でしょう。
Reducer 内が return
ばかりになって気分は良くないかもしれないので、(私はまだ使ったことがないですが、) TypeScript FSA Reducers もいいかもしれません。
Redux Thunk と一緒に
Redux で非同期処理をする際の一番簡単なやり方は Redux Thunk という Middleware を使うことだと思います。
単純で簡単な非同期処理が多い場合、学習のしやすさという面でおすすめできます。ただし、良くない意味でも自由度が高く、複雑な非同期処理を丁寧には書きにくいという欠点はあります。
似たような処理が多くなるのでライブラリ化には都合がよかったようで、 typescript-fsa-redux-thunk というライブラリは、型と非同期処理を書くだけで、上に挙げた 3 つの Action の Dispatch までやってくれる Thunk Action を定義できます。
簡単な処理を含む Thunk Action のコードをリファクタリングしていると、自然に typescript-fsa-redux-thunk の実装に似た形に落ち着きました。 Action が自動で Dispatch されることさえ覚えておけば、後はライブラリの挙動を気にする必要はありません。
12 | export const delayIncrementOperation = asyncActionCreator<void, void>( |
React Redux の connect
での mapDispatchToProps
定義のために bindActionCreators
を使うときはひと手間必要で、 countUp: thunkToAction(countUp.action)
のように thunkToAction
という関数と action
プロパティを使って Action そのものに(ガワだけ)変換する必要があります。
そのあとは TypeScript の ReturnType
などを使って、対象のコンポーネントに良しなに型をつけることができます。
16 | const mapDispatchToProps = (dispatch: Dispatch) => |
単体テストは後述の 2 つのライブラリに比べると簡単です。 Thunk Action のテストは、モック Store を作成しておいて Dispatch した後の Action 一覧をチェックするのが公式のおすすめ方法らしいです。 typescript-fsa-redux-thunk を利用していても、ここは変わりません。
30 | const store = mockStore({} as ApplicationState); |
サンプル実装: modules/thunk-duck.ts modules/_tests_/thunk-duck.ts ThunkView.tsx
Redux Saga と一緒に
Redux Saga は、非同期処理を含めた副作用のある処理を、 Saga という概念の元、 Effect を使いつつ Action をフックしたり Dispatch したりできる Middleware + ライブラリ です。
一見 yield
を使うなどして非常に複雑なようなことをやっているように見えますが、ある種の Reactive + async/await プログラミングなので、案外すぐに慣れるような気がします。複雑な非同期処理を行いたいならおすすめです。(スロットリングとかキャンセルとか簡単ですからね。)
一応 typescript-fsa-redux-saga というライブラリもありますが、パラメータの渡し方と Saga の単体テストに難があるのとあんまり記述量も減らず、かつ上記 3 つの Action を自動で Dispatch するだけなので、 Redux Saga を持ち出したくなるような用途の処理に対しては力不足かなと感じました。
actionCreator.async
で生成された ???_START
を起点に Saga を動かす場合は、 bindAsyncAction
のオプションで skipStartedAction: false
としておかないと無限ループに入るので気をつけましょう。
書き方はサンプルコードのコメントに残してあります。
13 | export const delayIncrementOperation = actionCreator<void>("DELAY_INCREMENT"); |
0 | // omit |
55 | // export const delayIncrementSaga = bindAsyncAction(delayIncrementActions)( |
こちらは bindActionCreators
で Action も Saga Action の区別をする必要がなく、すっきり書けます。また、 ReturnType
も同様にうまく効きます。
15 | const mapDispatchToProps = (dispatch: Dispatch) => |
何かの非同期 Action 完了後に、 Dispatch する側で処理をしたいような場合は、 Dispatch するときの変数に Promise の resolve と reject を渡し、 Saga の終わりにそれらを使うようにしてあげるのが一つのパターンのようです。Issue #161 · redux-saga/redux-saga
88 | const { promise, ...params } = action.payload; |
59 | onClick={() => |
Redux Thunk と比べると単体テストは若干面倒になりますが、 Redux Saga Test Plan を使えばなんとかテストできそうです。
単体テストでは testSaga
、統合テストなどでは expectSaga
を使うようで、前者での next()
の使い方に癖を感じました。テストコードには、テスト対象で対応する Effect をコメントとして残しておくほうが無難な気がします。また、間違えを恐れずに言えば、 next()
によりテスト対象の call()
をモック化できたりします。
65 | await testSaga(delayIncrementSaga).next() |
サンプル実装: modules/saga-duck.ts modules/_tests_/saga-duck.ts SagaView.tsx
redux-observable と一緒に
redux-observable も、非同期処理などを Reactive プログラミングを使いつつ、 Epic という関数の中で Action をフックしたり Dispatch したりできる Middleware + ライブラリです。
ReactiveExtensions や RxJS によるプログラミングに慣れていれば、すんなり使えるようになります。ウォッチ対象とする Action でフィルタリングし、処理を行い、別の Action 流すストリームを作成します。
RxJS 5 向けには typescript-fsa-redux-observable を使ってフィルタリングと TypeGuard が使えるようになります。
RxJS 6 向けには Issue #6 · m0a/typescript-fsa-redux-observable を参考に関数を作れば同じことができますが、 filter
に delayIncrementOperation.match
のようにマッチ関数を渡すだけで、後続の TypeGuard が正しく行われました。
75 | export const delayIncrementEpic: Epic<Action> = action$ => |
こちらも bindActionCreators
での Action の区別が必要なく、 ReturnType
もうまく効きます。非同期 Action 完了の処理の作り方についても、 Redux Saga の場合と同じです。
一方、 Epic の実装が風変わりになることがあります。例えば、上のコードのように複数の Action を Dispatch する場合には、複数の Action ストリームを merge
したものを実装する必要があります。
テストに関しては( RxJS を使う以上)かなり面倒で、 redux-observable でも Marble Test により行います。 from(Promise)
が使えないという制約があるため、モック化にも気をつかなければなりません。
どちらかと言えば、 RxJS 由来の悩みだけになるように感じました。
45 | testScheduler.run(({ hot, expectObservable }) => { |
サンプル実装: modules/observable-duck.ts modules/_tests_/observable-duck.ts ObservableView.tsx
最後に
TypeScript FSA で非同期処理する場合の、 Redux Thunk 、 Redux Saga 、 redux-observable の使用感を比較しました。
React Hooks の登場により Redux エコシステムがどう変化するかはまだ予測できませんが、 Redux そのものが下火にならなければこれらのライブラリも当面は残っていそうです。