アヒルさんがぐるぐる回っててかわいかったので飛びついてみたけど色々サンプルが動かなくて可愛くなかったので動くまでもがいた。
成果物
- githubに置いた
- pingボタンを押したらtrueになって1秒後にfalseになるサンプル。
-
https://jsbin.com/vayoho/6/edit
- ↑これとほぼおなじ
コード
簡素化のために1ファイルに収めている
import React from 'react'
import ReactDom from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { createEpicMiddleware } from 'redux-observable'
import { connect, Provider } from 'react-redux'
// rxjsハマり部分。後述
import 'rxjs/add/operator/mapTo'
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/delay'
// sampleにあるreducer
const pingReducer = (state = { isPinging: false }, action) => {
switch (action.type) {
case 'PING':
return { isPinging: true };
case 'PONG':
return { isPinging: false };
default:
return state;
}
}
// sampleにあるepic
const pingEpic = action$ => action$.filter(action => action.type === 'PING')
.delay(1000)
.mapTo({ type: 'PONG' })
// epicをmiddleware化 + Store化
const epicMiddleware = createEpicMiddleware(pingEpic);
const store = createStore(pingReducer, applyMiddleware(epicMiddleware))
// ボタン押したら文字が変わるcomponent
const PingComponent = ({dispatch, isPinging}) => {
return (
<div>
<div>isPinging: {isPinging.toString()}</div>
<div>
<button onClick={ (e) => dispatch({type: 'PING'}) }>Dispatch Ping</button>
</div>
</div>
)
}
// Build App
const App = () => {
// stateそのまま流す
let PingContainer = connect( state => state )(PingComponent)
return (
<Provider store={store}>
<PingContainer />
</Provider>
)
}
// Render App
ReactDom.render(
<App />,
document.body.appendChild(document.createElement('div'))
)
留意点・知見・ハマりどころ
Epicという用語
http://redux-observable.js.org/docs/basics/Epics.html
StreamなActionを受け取ってActionを返す。
これを定義してmiddlewareとして処理されるもの。
とりあえずこいつがredux-observable
のキモだと思って良さそう
RxJS 5の独特さ
JSBinサンプルの見てもちゃんと動いていてしばらくハマった部分。
たとえばこんなEpic
const pingEpic = action$ => action$.filter(action => action.type === 'PING')
.delay(1000)
.mapTo({ type: 'PONG' })
このまま走らせると
Uncaught TypeError: action$.filter is not a function
と言われる。
これでしばらく悩んでいたが、RxJS 5系では、各Operatorを明示的にimportしてやる必要があった。1
import 'rxjs/add/operator/mapTo'
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/delay'
このようにそれぞれimportすると動く。
rxjs/add/operator/filterあたりの中身を覗いてみるとわかるが、
Observable.prototype.filter = filter;
というような感じでObservableをprototype拡張している。
Epicで渡されるaction$
はredux-observableで管理されているActionObservableというものでObservable
を継承している。
「大量にimportするの面倒」というような場合は
import Rx from 'rxjs/Rx'
で呼び出して全Operatorのaddをすることも出来る模様。(副作用的な感じもするので、良いやり方なのかどうかは不明。。。)
備考:function bindによるRxの読み込み
RxJSのReadmeに記載されているが、「prototype拡張してんのやだなー」という場合は、function bind
を使う手法も一応ある。function-bindは今のところstage-0でかなり尖っているので注意。
$ npm i -D babel-plugin-transform-function-bind
.babelrcはこんな具合
{
"presets": ["es2015", "react"] ,
"plugins": [
"transform-function-bind"
]
}
そうするとこんな具合で書ける。
import { mapTo } from 'rxjs/operator/mapTo';
import { filter } from 'rxjs/operator/filter';
import { delay } from 'rxjs/operator/delay';
const pingEpic = action$ => action$
::filter(action => action.type === 'PING')
::delay(1000)
::mapTo({ type: 'PONG' })
stage-0なんて怖い!素で使いたい!
となると多分こんな具合。これはこれできつそう
const pingEpic = action$ => mapTo.call(
delay.call(
filter.call(action$, action => action.type === 'PING'),
1000
),
{ type: 'PONG' }
)
type絞るならfilter
使わずにofType
で良い
今回サンプルではfilter
を使ったが、ActionObeservable
にはofType
というのが用意されているので、これを使ったほうがキレイ目に書ける。
const pingEpic = action$ =>
action$.ofType('PING')
.delay(1000) // Asynchronously wait 1000ms then continue
.mapTo({ type: 'PONG' })
新しく別なactionを流せるだけ。元のactionに手を加える事は出来ない。
redux-thunkやmiddlewareだと出来ていたことで出来ないのが「actionの変更・加工・握りつぶし」的な作用。
例えばこんな感じの事は出来ない(多分)。
// PING actionだったら握りつぶしてPONGにするmiddleware
const fixupPingMiddleware = store => next => action => {
if(action.type !== 'PING'){
return next(action)
}
return next({
type: 'PONG'
})
}
createEpicMiddlewareの中身を見ると、上記のコードで言えば、こんなことをやっている感じ。
// PING actionだったらPONGも飛ばす
const fixupPingMiddleware = store => next => action => {
result = next(action)
store.dispatch({type: 'PONG'})
return result
}
そのため、もし上記の利用しないactionをreducerなどで影響が出ないようにしておく必要がある。actionがimmutableになっていると意識すると良いようだ。
そもそも「加工したり握りつぶしたり」というのはそんなにスマートなやり方では無いが、意識してやらないように心がけていないとやりたくなる事は多いので注意したい。
Epic作るのにRxJSに慣れてない時は、playground的なテストコード書いたら便利
RxJSに慣れていなかったので、Writning Testsを参考に、下記のようなplaygroundを作ると、色々試せて良い。
redux-mock-store
でactionが何が来たのか補足出来るので、最後にstore.getActions
してみると良い。
testにはava使ってみているけど、そこは別になんでも良い。
import test from 'ava'
import configureMockStore from 'redux-mock-store';
import { createEpicMiddleware, combineEpics } from 'redux-observable';
// operatorの追加忘れず
import 'rxjs/add/operator/map'
test('Epic Playground', t => {
// operatorとか色々試してみる
const pingEpic = action$ => action$
.ofType('PING')
.map( action => {
return {
type: 'PONG',
payload: action.payload
}
})
const epicMiddleware = createEpicMiddleware(pingEpic);
const mockStore = configureMockStore([epicMiddleware]);
let store = mockStore()
store.dispatch({ type: 'PING', payload: 1 })
console.log(store.getActions())
// output:
// [ { type: 'PING', payload: 1 }, { type: 'PONG', payload: 1 } ]
})
参考資料など
-
Learn RxJS
- とっつきやすいRxJSのドキュメント
-
MIGRATION.md
- RxJS4 -> RxJS5のmigration情報。結構混在しているので、詰まったら見直すと良い
ロゴ
どっかでこのロゴ見たことあるなと思ってたけどreduxのロゴ案として上がっていたやつだった。
https://github.com/reactjs/redux/issues/151#issuecomment-137833403
-
RxJSろくに理解してなかったせいでドはまりしたポイントではあるが、Javascriptとしてだいぶ独特な感じで「これは初見殺しじゃないですか」と思った。 ↩