Rx(RxJS)を学ぶのにredux-observableは結構良いんじゃないかという話

ここ最近redux-observableを触りまくって、過去何度も挫折してたRxがちょっとわかるようになってきた。

そこで「redux-observableって実はRx入門に良いのではないか?」と感じたのでちょっと自分なりの考えをまとめてみる。多分@ngrx/effectsでも同じ事が言えると思う

どんな点redux-observableはRx入門に良いと思うのか?

その1.「Everything is stream」はちょっと難しい。「Actions is stream」で考えると難易度下がる

Rxといえば「Everything is stream」。だが、Rx初学者の自分にとってハードルになっていたのではないかな?と感じた。

cycle.js、recomposeで挫折していた話

recomposeではcomponentFromStreamでvdom$とコンポーネントがstreamになっていたり、mapPropsStreamで props$もstreamに出来るようになっている。 だが「viewがstreamである」や「propsがstreamである」というのはRx初学者の自分にとっては流れ想像がしづらく、利点もわからなかった(ぶっちゃけ今でもこのへんはよくわかってない)。

cycle.jsなどではvdom$というviewがstreamとして扱われている。こちらも同様で、やっぱり利点が掴みづらかったり、Observableがstateを作り出すという世界観がどうにも難解に感じてしまった。

http://amagitakayosi.hatenablog.com/entry/2015/10/13/080000http://r7kamura.hatenablog.com/entry/2015/10/04/062333

most, baconなどのReactiveライブラリで挫折していた話

most, baconのような別のReactiveライブラリもサンプルがある。 これらはだいたいキーイベントやクリックイベントをStreamとして例示されている。

// baconの例
var up   = $('#up').asEventStream('click');
var counter = up.map(1).scan(0, function(x,y) { return x + y });
counter.assign($('#counter'), 'text');

jQueryとの組み合わせでの使い方はボンヤリわかるものの、「どうすると利点になるのか?」はちょっとわかりづらく、更に不幸にもReactとの組み合わせも全く想像がつかなかった。

本来ここでRxをマスターしてる人間だったらrecomposeを組み合わせるような話になるんだと思う。

redux-observableでは「Actions is Stream」で良かった

redux-observableにおいては、「ActionだけがStreamである」と割り切って考えられる。 「Actionがずーっとフローの中を流れ続けている」というのはReduxの概念とぴったり合うし、それを補足して何か行う、という概念が自分の中に割とスッと入ってきた。

その2. Hot / Coldについてひとまず考えなくていい。

よくRxの話で出て来るこのHot / Coldの話。 でもredux-observableの世界において、あまり考える事が無い(自分もまだちゃんとわかってないが、redux-observableにおいてはそんなに困らない)

基本的にEpicはActionが来て初めて動くので、Coldの組み合わせだけ意識すれば良い。

// PINGというaction$
const pongEpic = (action$) => action$
.ofType("PING")
.mapTo({
type: "PONG"
})

その3. あんまり複雑にしなくても良い。複雑化しそうならActionを発行して分岐させちゃえる

今のところ「Rxが複雑すぎて読めない」みたいなパターンになっていない。
ユースケースをまだまだ複雑なことやっていないというのはあるけど、おそらくこの「一つのEpicが複雑になりすぎるなら一回actionを発火させてオペレータを断ち切る」事が出来るからではないかと仮説を立てている。

例えば、bookを取得して、同時にそのままauthorを取得するとして

const fetchBookAndAuthorEpic = (action$) => 
action$.ofType("FETCH_BOOK_REQUEST")
.mergeMap( action => fetchBook(action.id) )
.map( response => response.data )
.mergeMap( data => Observable.merge(
Observable.of({
type: "FULLFILLED_BOOK",
data
}),
Observable.fromPromise(fetchAuthor(data.author) )
.map( response => response.data )
.map( data => ({
type: "FULLFILLED_AUTHOR",
data
}))
))

こんな具合になる。結構きつくなりがち。 redux-observableであれば適宜actionを発火することでepicを分断出来る。あんまりやりすぎると逆に可読性下がったりテストしづらい事があるが、手段として分割出来ると小さくRxを学びやすい

const fetchBookEpic = (action$) => 
action$.ofType("FETCH_BOOK_REQUEST")
.mergeMap( action => fetchBook(action.id) )
.map( response => ({
type: "FULLFILLED_BOOK",
data: response.data
}))
// bookを習得したらauthorをリクエストする中間epic
const requestAuthorEpic = (action$) =>
action$.ofType("FULLFILLED_BOOK")
.map(({data}) => ({
type: "FETCH_AUTHOR_REQUEST",
author: data.author
}))
const fetchAuthorEpic = (action$) => 
action$.ofType("FETCH_AUTHOR_REQUEST")
.mergeMap(action => fetchAuthor(action.author) )
.map( response => ({
type: "FULLFILLED_AUTHOR",
data: response.data
}))

その4. テストが書きやすい

UnitTestが書けると、そこを遊び場に出来るので学習速度が上がるObservable.of(元のaction)でmockなAction Streamを作り、それを利用してさくっとテストが書ける

it("ping epic", (done) => {
// inputとなるaction
const mockAction = Observable.of({type: "PING"})
pingEpic(mockAction)
.subscribe( result => {
// 期待する出力
assert.deepEqual(result, {type: "PONG"}
)
done()
})
})
})

下記にやり方をまとめている。
http://qiita.com/inuscript/items/47984cb5fbfb6acf09d5

現在これは公式的なやり方ではないが、割と近い手法がPull Reqされている。 
https://github.com/redux-observable/redux-observable/issues/204

epicをゴニョゴニョいじりながらああでもないこうでもないとoperatorをいじくり回してみると、かなり学習が進む。 自分の肌感でも、ペアプロを通して他者に教えてみてもこのやり方でやるのが学習速度が早く感じている

逆にredux-observableのどんなところがRx入門に向いてないか

その1. redux(react)を前提にしている

そりゃそうなんだけど、react / reduxをほぼ前提にしているので、そこに全く興味が無い人には向いてなかろうと思われる
(一応、reduxもredux-observableもreact以外で使えると謳い文句にしてるけど、多分やる人いなそう・・・)

その2. ドキュメントとかがRx出来る人前提っぽくなってる

これもある程度しょうがないのだけど、redux-observableは結構「お前、Rx出来るよな?」というところがスタートラインでドキュメントが書かれている感じがあるので、初期のつっかかりが大きい。

自分が躓いた箇所はこのへんでまとめているhttp://qiita.com/inuscript/items/dae3f08c1f55065ba413

公式RxJSのドキュメントもわりと慣れるまで辛いのでとにかくRxJSの部分はLearn RxJSを読み漁りまくるのがお勧め。

実際redux-observable使ってみてどうだったか?

Rxを学ぶという部分にも最適だったが、実務上でもやっぱり色々利点は感じることが出来た。 以下雑多な感想(利点多め)

  • 「非同期解決以外使わないだろ」と思ってたObservableの処理が意外と使うポイントあった(キーイベントのthrottle、複数actionを待っての処理など・・・)
  • 色々出来るので無駄にComponentでlocal state持ったりロジック書いたりに済んでいる箇所が結構ある
  • redux / redux-observableが腐ってもRxの知識が他で使えるのは嬉しい(Rxの学習をコストと捉えるかどうか?という部分。個人的には色々視界が広がった気持ちになっている)
  • 学習コストはちょっと高い。しかし無理してRxで書かずにactionやcomponentで解決する逃げ道があるので、段階的に無理なく導入していける。

ということで、Rxを既に習得している人にredux-observableは実用的なものとしてお勧め出来るし、Rxを学びたいという人にも十分おすすめ出来そうだと感じている。