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
redux
と redux-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.js
の dummyAuth関数
にて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'))
の実行により以下の流れで処理が実行されています。
userReducer
がUSER_FETCH_REQUESTED
のAction Type
を受け取ってます。- ただ、
userReducer
自体は、USER_FETCH_REQUESTED
を受け取ってもstate
の変更を行いません。
- ただ、
Saga
でUSER_FETCH_REQUESTED
のAction Type
を受け取り、fetchUser
を起動しています。fetchUser
内で非同期処理が成功して、USER_FETCH_SUCCEEDED
のAction Type
を発行してます。
userReducer
がUSER_FETCH_SUCCEEDED
のAction Type
を受け取ってます。payload
に設定されたデータをもとにstate
を更新しています。