Redux-Sagaによる非同期処理の動作確認

Redux Sagaは、副作用のある処理(非同期処理など)を独立した個別の処理で実行し、Reduxの可読性を下げずに実装する方法を提供してくれます。ただ、文章だけでは概念を理解しづらいです。ここでは、Redux Sagaを利用したシンプルなサンプル処理に console.log() を入れて、どのタイミングでRedux Sagaの処理が実行されるのか確認していきます。

Redux Sagaの動作確認用コード

Redux Sagaのみの動作確認にReactは不要なので、nodeで動作確認します。

redux, redux sagaをインストール

動作確認用のプロジェクトを構築します。

mkdir react-redux-saga
cd react-redux-saga/
npm init

reduxredux-saga をインストールします。

npm install --save redux redux-saga

動作確認用ソースのフォルダ構造

今回、以下フォルダ構造で動作確認用の処理を実装します。

├── redux
│   ├── user
│   │   ├── user.actions.js
│   │   ├── user.reducer.js
│   │   ├── user.sagas.js
│   │   └── user.types.js
│   ├── root-reducer.js
│   ├── root-saga.js
│   └── store.js
├── app.js
├── package-lock.json
└── package.json

Action Typesを定義
 - redux/user/user.types.js

const UserActionTypes = {
  USER_FETCH_REQUESTED: 'USER_FETCH_REQUESTED',
  USER_FETCH_SUCCEEDED: 'USER_FETCH_SUCCEEDED',
  USER_FETCH_FAILURE: 'USER_FETCH_FAILURE',
}

export default UserActionTypes

Action Creatorsを定義
 - redux/user/user.actions.js

import UserActionTypes from './user.types'

export const userFetchRequested = (userId, password) => {
  console.log('[Log] [File: redux/user/user.actions.js] [Function: userFetchRequested]')
  return ({
    type: UserActionTypes.USER_FETCH_REQUESTED,
    payload: { userId, password }
  })
}
export const userFetchSucceeded = user => {
  console.log('[Log] [File: redux/user/user.actions.js] [Function: userFetchSucceeded]')
  return ({
    type: UserActionTypes.USER_FETCH_SUCCEEDED,
    payload: user
  })
}
export const userFetchFailure = error => {
  console.log('[Log] [File: redux/user/user.actions.js] [Function: userFetchFailure]')
  return ({
    type: UserActionTypes.USER_FETCH_FAILURE,
    payload: error
  })
}

Reducersを定義
 - redux/user/user.reducer.js

import UserActionTypes from './user.types'

const INITIAL_STATE = {
  currentUser: null,
  error: null
}

const userReducer = (state = INITIAL_STATE, action) => {
  console.log(`[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: ${action.type}]`)
  switch (action.type) {
    case UserActionTypes.USER_FETCH_SUCCEEDED:
      return {
        ...state,
        currentUser: action.payload,
        error: null
      }
    case UserActionTypes.USER_FETCH_FAILURE:
      return {
        ...state,
        error: action.payload
      }
    default:
      return state
  }
}

export default userReducer

Sagasを定義
 - redux/user/user.sagas.js

import { takeLatest, put, all, call } from 'redux-saga/effects'

import UserActionTypes from './user.types'
import { userFetchSucceeded, userFetchFailure } from './user.actions'

const dummyAuth = (userId, password) => {
  return new Promise((resolve, reject) => {
    return password === 'xxx'
      ? resolve({ userId, name: 'wakuwaku' })
      : reject({ message: 'dummy auth error' })
  })
}

function* fetchUser({ payload: { userId, password } }) {
  console.log('[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Start]')
  try {
    const user = yield dummyAuth(userId, password)
    yield put(userFetchSucceeded(user))
    console.log('[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Done userFetchSucceeded]')
  } catch (error) {
    yield put(userFetchFailure(error))
    console.log('[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Done userFetchFailure]')
  }
}

function* onUserFetchStart() {
  console.log('[Log] [File: redux/user/user.sagas.js] [Function: onUserFetchStart] [Note: Start]')
  yield takeLatest(UserActionTypes.USER_FETCH_REQUESTED, fetchUser)
  console.log('[Log] [File: redux/user/user.sagas.js] [Function: onUserFetchStart] [Note: Done]')
}

export function* userSagas() {
  console.log('[Log] [File: redux/user/user.sagas.js] [Function: userSagas] [Note: Start]')
  yield all([
    call(onUserFetchStart),
  ])
  console.log('[Log] [File: redux/user/user.sagas.js] [Function: userSagas] [Note: Done]')
}

Reducersを1つにまとめる
 - redux/root-reducer.js

今回1つしか存在しないので、まとめる必要はないです。もし、複数ある場合 combineReducers() でまとめておきます。

import { combineReducers } from 'redux'

import userReducer from './user/user.reducer'

const rootReducer = combineReducers({
  user: userReducer,
})

export default rootReducer

Sagasを1つにまとめる
 - redux/root-saga.js

今回1つしか存在しないので、まとめる必要はないです。もし、複数ある場合 all() でまとめておきます。

import { all, call } from 'redux-saga/effects'

import { userSagas } from './user/user.sagas'

export default function* rootSaga() {
  console.log('[Log] [File: redux/root-saga.js] [Function: rootSaga] [Note: Start]')
  yield all([call(userSagas)])
  console.log('[Log] [File: redux/root-saga.js] [Function: rootSaga] [Note: Done]')
}

Storeを生成
( SagaMiddlewareをStoreに接続 )
 - redux/store.js

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import rootReducer from './root-reducer'
import rootSaga from './root-saga'

console.log('[Log] [File: redux/store.js] [Note: Before createSagaMiddleware()]')
const sagaMiddleware = createSagaMiddleware()
const initialState = {}
const store = createStore(
  rootReducer,
  initialState,
  applyMiddleware(sagaMiddleware)
)

console.log('[Log] [File: redux/store.js] [Note: Before sagaMiddleware.run(rootSaga)]')
sagaMiddleware.run(rootSaga)

export default store

createSagaMiddleware() でSagaMiddlewareを生成し、生成したSagaMiddlewareをStoreに接続します。
その後、sagaMiddleware.run(rootSaga) でsagaを開始させます。

動作確認用処理
 - app.js

import { userFetchRequested } from './redux/user/user.actions'
import store from './redux/store'

(async () => {
  const sleep = (second) => new Promise(resolve => {
    console.log(`[Log] [File: app.js] [Note: ${second}秒間Sleep]`)
    setTimeout(resolve, second * 1000)
  })
  console.log()

  console.log('############ state(初期値) ############')
  console.log(`[State] ${JSON.stringify(store.getState())}`)
  console.log()

  console.log('############ 動作確認1 (userFetchRequested Success) ############')
  store.dispatch(userFetchRequested(1, 'xxx'))
  console.log(`[State] ${JSON.stringify(store.getState())}`)
  await sleep(1)
  console.log(`[State] ${JSON.stringify(store.getState())}`)
  console.log()

  console.log('############ 動作確認2 (userFetchRequested Error) ############')
  store.dispatch(userFetchRequested(1, 'yyy'))
  console.log(`[State] ${JSON.stringify(store.getState())}`)
  await sleep(1)
  console.log(`[State] ${JSON.stringify(store.getState())}`)
})()

redux/user/user.sagas.jsdummyAuth関数 にてpasswordが

  • xxx であれば resolve
  • xxx 以外であれば reject

となるように実装しています。

そのため上記処理で、userFetchRequested が「成功したパターン」と「失敗したパターン」の動作確認をすることができます。

実行

前準備|babel導入

import などnodeでは利用できないので、babelを導入しておきます。

npm install --save-dev \
@babel/cli \
@babel/core \
@babel/node \
@babel/plugin-proposal-object-rest-spread \
@babel/preset-env

.babelrc ファイルを作成して以下内容を記述します。

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread"
  ]
}

実行結果

$ npx babel-node app.js 
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: @@redux/INITq.0.7.7.j.f]
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: @@redux/PROBE_UNKNOWN_ACTIONs.h.k.9.n.i]
[Log] [File: redux/store.js] [Note: Before createSagaMiddleware()]
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: @@redux/INITq.0.7.7.j.f]
[Log] [File: redux/store.js] [Note: Before sagaMiddleware.run(rootSaga)]
[Log] [File: redux/root-saga.js] [Function: rootSaga] [Note: Start]
[Log] [File: redux/user/user.sagas.js] [Function: userSagas] [Note: Start]
[Log] [File: redux/user/user.sagas.js] [Function: onUserFetchStart] [Note: Start]
[Log] [File: redux/user/user.sagas.js] [Function: onUserFetchStart] [Note: Done]

############ state(初期値) ############
[State] {"user":{"currentUser":null,"error":null}}

############ 動作確認1 (userFetchRequested Success) ############
[Log] [File: redux/user/user.actions.js] [Function: userFetchRequested]
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: USER_FETCH_REQUESTED]
[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Start]
[State] {"user":{"currentUser":null,"error":null}}
[Log] [File: app.js] [Note: 1秒間Sleep]
[Log] [File: redux/user/user.actions.js] [Function: userFetchSucceeded]
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: USER_FETCH_SUCCEEDED]
[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Done userFetchSucceeded]
[State] {"user":{"currentUser":{"userId":1,"name":"wakuwaku"},"error":null}}

############ 動作確認2 (userFetchRequested Error) ############
[Log] [File: redux/user/user.actions.js] [Function: userFetchRequested]
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: USER_FETCH_REQUESTED]
[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Start]
[State] {"user":{"currentUser":{"userId":1,"name":"wakuwaku"},"error":null}}
[Log] [File: app.js] [Note: 1秒間Sleep]
[Log] [File: redux/user/user.actions.js] [Function: userFetchFailure]
[Log] [File: redux/user/user.reducer.js] [Function: userReducer] [Action Type: USER_FETCH_FAILURE]
[Log] [File: redux/user/user.sagas.js] [Function: fetchUser] [Note: Done userFetchFailure]
[State] {"user":{"currentUser":{"userId":1,"name":"wakuwaku"},"error":{"message":"dummy auth error"}}}

sagaMiddleware.run(rootSaga) の実行により rootSaga が起動されています。

store.dispatch(userFetchRequested(1, 'xxx')) の実行により以下の流れで処理が実行されています。

  • userReducerUSER_FETCH_REQUESTEDAction Type を受け取ってます。
    • ただ、userReducer 自体は、USER_FETCH_REQUESTED を受け取っても state の変更を行いません。
  • SagaUSER_FETCH_REQUESTEDAction Type を受け取り、fetchUser を起動しています。
    • fetchUser 内で非同期処理が成功して、USER_FETCH_SUCCEEDEDAction Type を発行してます。
  • userReducerUSER_FETCH_SUCCEEDEDAction Type を受け取ってます。
    • payload に設定されたデータをもとに state を更新しています。

参考