JavaScript
redux
redux-saga
redux-thunk
redux-observable

Reduxの非同期処理ライブラリをいろいろ試してみた!

今回選んだライブラリは、それぞれのアプローチで問題を解決しているので、面白いと思い選びました。
タイトルでいろいろとは言ってるものの、使ってみたのは以下になります。

  • redux-thunk
  • redux-saga
  • redux-observable

トレンド

スクリーンショット 2017-06-29 19.28.57.png

まだまだまだthunkが人気です。
sagaは半年で2倍くらいに成長しています。

今回作ったもの

今回は、クリックすると「PING」に変わり、2秒後に「PONG」に戻るものを作りました。
これ自体は、redux-observableのチュートリアルに載っているものをredux-thunkとredux-sagaに展開しました。

pingpong.gif

reduxは単体でも使えますが、今回はReactと組み合わせています。

実装

ベース

ベースとなるコードにには、非同期処理を書いていません。
クリックすると「PING」となるだけで止まってしまいます。

手前味噌ですが、よくわからない人は以前書いた記事を参考にして下さい。

action.js
import { createAction } from 'redux-actions'

export const PONG = 'PONG'
export const pong = createAction(PONG)

export const PING = 'PING'
export const ping = createAction(PING)
component.js
import React, { Component } from 'react'

class PingPong extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    const text = this.props.isPingPong ? 'PING' : 'PONG'
    return (
      <div onClick={this.props.onClickHandler}>{text}</div>
    )
  }
}

export default PingPongComponent
configureStore.js
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import reducers from '../reducers'

export default function configureStore(initialState) {
  const store = createStore(
    reducers,
    initialState,
    applyMiddleware(
      createLogger()
    )
  )
  return store
}

container.js
import React from 'react'
import { connect } from 'react-redux'
import PingPongComponent from '../components'
import { ping } from '../actions'

const mapStateToProps = (state) => {
  return state
}

const mapDispatchToProps = (dispatch) => {
  return {
    onClickHandler: () => {
      dispatch(ping())
    }
  }
}

const PingPongContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(PingPongComponent)

export default PingPongContainer
router.js
import React from 'react'
import { Route } from 'react-router-dom'
import container from './containers'

const router = () => {
  return (
    <div>
      <Route exact={true} path="/" component={container}/>
    </div>
  )
}

export default router
app.js
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import Router from './router'
import configureStore from './configureStore'

const preloadedState = {}
const store = configureStore(preloadedState)

render(
  <Provider store={store}>
    <BrowserRouter>
      <Router />
    </BrowserRouter>
  </Provider>,
  document.getElementById('content')
)

redux-thunk

  • reduxの非同期周りでもっとも使用されているライブラリ。
  • F8のアプリなどにも使用されている。
  • reduxと作者がいっしょらしいので、準公式middleware。
  • actionに非同期処理を押し込んで、それをdispatchする感じで呼んでいる。
action.js
import { createAction } from 'redux-actions'

export const PONG = 'PONG'
export const pong = createAction(PONG)

export const PING = 'PING'
export const ping = createAction(PING)

export function asyncPingPong() {
  const delayTime = 2000
  return (dispatch) => {
    dispatch(ping())
    setTimeout(() => {
      dispatch(pong())
    }, delayTime)
  }
}
container.js
const mapDispatchToProps = (dispatch) => {
  return {
    onClickHandler: () => {
      dispatch(asyncPingPong())
    }
  }
}
configureStore.js
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'
import reducers from '../reducers'

export default function configureStore(initialState) {
  const store = createStore(
    reducers,
    initialState,
    applyMiddleware(
      thunk,
      createLogger()
    )
  )
  return store
}

感想

  • actionが肥大化するので、あまり好みではないが手軽。
  • 作りがシンプルで学習コストは低く感じる。

redux-saga

  • generator関数を用いて、非同期処理を同期的に書ける。
  • 非同期処理を別スレッドに切り出すイメージ。
sagas.js
import { delay } from 'redux-saga'
import { takeEvery, put } from 'redux-saga/effects'
import { PING, pong } from '../actions'

function* sagaPingPong() {
  yield delay(2000)
  yield put(pong())
}

export function* rootSaga() {
  yield takeEvery(PING, sagaPingPong)
}
configureStore.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { createLogger } from 'redux-logger'
import reducers from '../reducers'
import { rootSaga } from '../sagas'

const sagaMiddleware = createSagaMiddleware()

export default function configureStore(initialState) {
  const store = createStore(
    reducers,
    initialState,
    applyMiddleware(
      sagaMiddleware,
      createLogger()
    )
  )
  sagaMiddleware.run(rootSaga)
  return store
}

感想

  • 普段全く使っていないジェネレータ関数を触るきっかけになった。(ジェネレータ関数使って見たかった)
  • redux-thunkのAction汚染から解放。
  • チーム開発に導入するとなると全体的に学習コストは高めだと思うけど、ジェネレータ関数も標準機能だからいずれはみんな通る道なのかも。

参考

redux-observable

  • 最近流行りのRx〇〇。
  • middlewareでActionを受け取って、Actionを返す処理をEpicと呼ぶ。
  • rxjsなどを使用したことがある人だとオススメ。
  • Netflixが推進している?

*redux-observable is a community-driven project and is not officially affiliated with Netflix. (redux-observableはコミュニティのプロジェクトで、オフィシャルなNetflixのやつではないよ)

image.png

epic.js
import { createEpicMiddleware } from 'redux-observable'
import { PING, pong } from '../actions'
import 'rxjs/add/operator/delay'
import 'rxjs/add/operator/mapTo'

const pingpongEpic = (action$) => {
  return action$.ofType(PING)
                .delay(2000)
                .mapTo(pong())
}

const epicMiddleware = createEpicMiddleware(
  pingpongEpic
)
export default epicMiddleware

configureStore.js
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import reducers from '../reducers'
import epicMiddleware from '../epics'

export default function configureStore(initialState) {
  const store = createStore(
    reducers,
    initialState,
    applyMiddleware(
      epicMiddleware,
      createLogger()
    )
  )
  return store
}

感想

  • 処理のフローが個人的に1番見やすい。
  • 小さいプロダクトにrxjsを入れるのは何となく気が引けるが、それを犠牲にしたくなるほどの可読性。

まとめ

まだ使いこなすというところまではいってませんが、どれもアプローチの仕方が面白かったです。
同じ内容でLTさせてもらった時は、sagaが個人的に好みという話で締めてしまったが、少し触っているうちにobservableがしっくりくるようになってきました。
また他のアプローチが出てきたら、試して見たいと思います!