React 復習メモ(その 3)

Redux-Thunk を試す

前回は、Redux を使用して同期的なイベント処理の素振すぶりを行なった。 今日はもう一歩がんばって、Redux-Thunk を用いた非同期イベントのハンドリングを試そうと思う。

Redux-Thunk とは

公式の説明によれば、こういうことらしい。

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met.

Redux Thunk ミドルウェアを使用すると、(Action オブジェクトではなく)関数を返す ActionCreator を作成できる。

Thunk は、以下のことができる。

  • Action の dispatch を遅延させる
  • 特定の条件を満たす場合にのみ Action を dispatch する

上記を組み合わせることで、たとえば非同期処理の完了を待ってから Action を dispatch する・非同期処理の結果に応じて dispatch する Action を変えるといった芸当が可能になるらしい。

いきなり Redux-Thunk を触ると何がなんだか分からなくなるので、まずは前回の復習から始めよう。

前回の復習

このエントリの前提となっている記事がこちら

tercel-s.hatenablog.jp

上記のエントリに書いた Redux 使用例のサンプルコードが今回の出発点である。 相変わらず、何のことはないただのカウンタアプリだ。

できるだけ単純化したコードなので、一旦、Redux の構成要素である Action, Reducer, Store, View がそれぞれ何を担っているのかを思い出してから先に進む*1

src/App.js
import React from 'react'
import { connect } from 'react-redux'

// Action Creators
const ActionType = {
  INCREMENT_COUNTER: 'INCREMENT_COUNTER',
  DECREMENT_COUNTER: 'DECREMENT_COUNTER'
}

const increment  = () => ({ type: ActionType.INCREMENT_COUNTER })// カウントアップ
const decrement  = () => ({ type: ActionType.DECREMENT_COUNTER })// カウントダウン


// Reducer
const INITIAL_STATE = { counter: 0 }
export function counter(state = INITIAL_STATE, action) {
  switch (action.type) {
    case ActionType.INCREMENT_COUNTER:     // カウントアップ
      return Object.assign({}, state,
        {counter: state.counter + 1 })
    case ActionType.DECREMENT_COUNTER:     // カウントダウン
      return Object.assign({}, state,
        { counter: state.counter - 1 })
    default:
      return state
  }
}


// Reactコンポーネント
let App = (state) => {
  const dispatch = state.dispatch
  return (
    <div>
      <div>{state.counter}</div>
      <button
        onClick={() => dispatch(increment())}>+</button>
      <button
        onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}


// ReactとReduxの接続
function mapStateToProps(state) { return state }
App = connect(mapStateToProps)(App)

export default App
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App, { counter } from './App'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import './index.css'
import registerServiceWorker from './registerServiceWorker'

const store = createStore(counter)

ReactDOM.render(
  <Provider store={store}><App /></Provider>,
  document.getElementById('root')
);

registerServiceWorker();

Redux-Thunk 版

いよいよ上記に Redux-Thunk を適用してみる。 ターミナルを起動し以下を打ち込もう。

$ npm i --save redux-thunk

次に、このへんこのへんを参考に、非同期 Action incrementAsync を実装してみる。

incrementAsync の戻り値は「dipatch を引数に取る関数」となっているっぽい。 この関数の中で、processingincrementを dispatch している。

非同期処理を模すため、「+」ボタンを押してからカウンタが更新されるまで500ミリ秒くらい遅延するようにしてある。

src/App.js
import React from 'react'
import { connect } from 'react-redux'

// Action Creators
const ActionType = {
  PROCESSING: 'PROCESSING',
  INCREMENT_COUNTER: 'INCREMENT_COUNTER',
  DECREMENT_COUNTER: 'DECREMENT_COUNTER'
}

const processing = () => ({ type: ActionType.PROCESSING })      // 処理中
const increment  = () => ({ type: ActionType.INCREMENT_COUNTER })// カウントアップ
const decrement  = () => ({ type: ActionType.DECREMENT_COUNTER })// カウントダウン

const incrementAsync = () =>  // ★ カウントアップ(非同期)
  dispatch => {
    dispatch(processing())    // processing action を dispatch
    setTimeout(() => {
      dispatch(increment())   // 500ミリ秒後に increment action を dispatch
    }, 500)
  }


// Reducer
const INITIAL_STATE = { counter: 0, processing: false }
export function counter(state = INITIAL_STATE, action) {
  switch (action.type) {
    case ActionType.PROCESSING:            // 処理中
      return Object.assign({}, state,
        { processing: true })
    case ActionType.INCREMENT_COUNTER:     // カウントアップ
      return Object.assign({}, state,
        {
          counter: state.counter + 1,
          processing: false
        })
    case ActionType.DECREMENT_COUNTER:     // カウントダウン
      return Object.assign({}, state,
        { counter: state.counter - 1 })
    default:
      return state
  }
}


// Reactコンポーネント
let App = (state) => {
  const dispatch = state.dispatch
  return (
    <div>
      <div>{state.processing ?
        '処理中' : state.counter}</div>

      {/* 処理中の場合はボタンを押せなくする */}
      <button
        disabled={state.processing}
        onClick={() =>
          dispatch(incrementAsync())}>+</button>
      <button
        disabled={state.processing}
        onClick={() =>
          dispatch(decrement())}>-</button>
    </div>
  )
}


// ReactとReduxの接続
function mapStateToProps(state) { return state }
App = connect(mapStateToProps)(App)

export default App
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import App, { counter } from './App'

import './index.css'
import registerServiceWorker from './registerServiceWorker'

const store = createStore(counter, applyMiddleware(thunk))

ReactDOM.render(
  <Provider store={store}><App /></Provider>,
  document.getElementById('root')
)

registerServiceWorker()

今日はそろそろ眠いのでここまで。

*1:ちょっと汚かったので多少リファクタリングしてみた。