redux-saga 使用教學

何謂 redux-saga?

  • redux 的 middleware
  • 用於管理 Redux 非同步資料流的處理
  • 將 saga pattern 實踐在 react/redux 應用上

特性

  • 集中處理 redux 副作用 (side effect, 一般泛指非同步) 問題
  • 被實現為 generator (es6 feature)
  • 類似 redux-thunk middleware
  • watch/worker(監聽->執行) 的工作形式

優點

  • 保持 action 和 reducer 的簡單純粹
  • 可將商業邏輯從 React Component 中抽出來,保持 View 的乾淨
  • 程式碼更容易被測試和閱讀
  • redux-saga 提供了豐富的 API 實現 saga pattern
  • 處理複雜非同步問題,如果商業邏輯複雜,快讓 redux-saga 來拯救你
  • 使用 saga 處理資料流非常清晰,雖然一開始寫會比較繁瑣,但日後程式碼會更好維護

缺點

  • 其他的 middleware 可能較難以和 redux-saga 搭配,使用時或許要花費一些代價,用 redux-saga 重構部分程式
  • babel-loader 轉出來的 source map 會錯位,要手動加 debugger 比較好測試

用程式碼快速了解 redux-saga 特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import {call, take, fork} from 'redux-saga/effects';
import * as ActionTypes from './actionTypes';
/**
* worker
* API 呼叫,並依據結果(成功、失敗) 更新redux state
*/
function* loadCourse() {
yield put({ type: LOAD_DATA_REQUEST });
try {
// 使用call呼叫 api
const result = yield call(api.getCourse, payload);
// do your staff with result
// ex: normalize format
yield put({
type: LOAD_DATA_SUCCESS,
payload: result,
});
} catch (error) {
yield put({
type: LOAD_DATA_FAILURE,
error
});
}
}
/**
*watcher
*/
function* watchGetCourse() {
while (true) {
// 創建一個監聽“程序”
yield take(ActionTypes.GET_COURSE)
// 監聽到 GET_COURSE event後,依序啟動下面的程序
yield fork(loadCourse);
...
}
}

簡單說明一下上述的程式碼:

  1. function 後面要加上 *,這是 generator 的寫法
  2. watchGetCourse 會在 app 執行期間持續地監聽 GET_COURSE action
  3. 當GET_COURSE action 被執行時,調用 loadCourse 這個 saga function
  4. 若 api 呼叫成功,dispatch LOAD_DATA_SUCCESS action,將資料更新回 store
  5. 若 api 呼叫失敗,dispatch LOAD_DATA_FAILURE action,將 error log 寫進 store

Effects

Effect 指的是一個 javascript object,裡面包含描述副作用的訊息,redux-saga 實作了豐富的 Effect API 幫助我們實現 Saga Effect。在 redux-saga 世界裡,所有的 Effect 都必須被 yield,由 yield 傳遞给 sagaMiddleware 執行。原則上來說,所有的 yield 後面也只能跟 Effect。

常用 Effects 的功能介紹:

1、take

等待 redux dispatch 匹配某個 pattern 的 action。
在上面例子中,先等待一個取得課程的 action ,然後執行取得課程的 saga:

1
2
3
4
5
6
7
8
9
while (true) {
yield take(ActionTypes.GET_COURSE)
yield fork(loadCourse);
}
while (true) {
const {id} = yield take(ActionTypes.GET_COURSE)
yield fork(loadCourse, id);
}

2、takeEvery

作用和 take 一樣,不過不用寫 while 迴圈了,取得 action payload 的方式也不太一樣

1
2
3
4
5
6
7
function* login({ username, password }) {
// do login
}
function* watchLogin()
yield takeEvery('LOGIN_REQUEST', login);
}

3、fork

調用 saga function,可以返回執行結果。

1
2
yield fork(loadCourse);
const { response } = yield fork(api.getCourse);

4、call

與 fork 作用相同,差別在 fork 是非阻塞(non-block) 型調用,call 是阻塞(block) 型調用,所以對有順序性的操作你可以像底下這樣子寫。

1
2
const categories = yield call(api.getCourseCategories);
const courses = yield call(api.getCourses, {categories});

5、put

作用和 redux 中的 dispatch 相同。

1
2
3
4
yield put({ type: 'GET_COURSE' });
yield put({ type: 'GET_COURSE_REQUEST', id });
yield put({ type: 'GET_COURSE_SUCCESS', data });
yield put({ type: 'GET_COURSE_FAILURE', error });

6、select

作用和 redux thunk 中的 getState 相同,若 saga 中需要使用 redux state 時,可以透過 select 來取用。

1
2
3
4
5
6
const id = yield select(state => state.id);
if (id) {
yield call(api.getCourse(id));
} esle {
throw new Error('id is undefined');
}

7、all

如果我們同時要 call 好幾支不同的 api,你可以這樣子寫,作用和 Promise.all 效果一樣,會等所有 api call 執行完後才往下繼續執行

1
2
3
4
5
6
7
8
9
const [
response: { course },
response: { user },
response: { order },
] = yield all({
call(api.getCourse, courseId),
call(api.getUser, userId),
call(api.getOrder, orderId),
});

如何在專案中導入?

1、安裝 redux-saga

1
npm install redux-saga --save

或是透過 yarn

1
yarn add redux-saga

2、加入 Saga Middleware

store 中加入 sagaMiddleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { createStore as _createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware, { END } from 'redux-saga';
import reducer from './reducer';
import sagaManager from './sagaManager';
export default function createStore(history, client, data) {
const sagaMiddleware = createSagaMiddleware();
const middleware = [
reduxRouterMiddleware,
sagaMiddleware
];
let enhancers = [applyMiddleware(...middleware)];
const finalCreateStore = compose(...enhancers)(_createStore);
const store = finalCreateStore(reducer, data);
sagaManager.runSagas(sagaMiddleware);
if (__DEVELOPMENT__ && module.hot) {
module.hot.accept('./reducer', () => {
store.replaceReducer(require('./reducer'));
});
}
store.runSaga = sagaMiddleware.run;
store.close = () => store.dispatch(END);
return store;
}

3、建立 sagaManager.js

將專案中所有的 saga 引用進來,並在 store 中透過該sagaManager.js 執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { fork, cancel, take } from 'redux-saga/effects';
import authSaga from './modules/auth/authSaga';
export function* sagas() {
yield [
...authSaga,
];
}
const rootSaga = [sagas];
export default {
runSagas(sagaMiddleware) {
rootSaga.forEach((saga) => sagaMiddleware.run(saga));
},
};

With Hot Reload

目前 redux-saga 官方尚未支援 Hot reload,必須要在專案中做些修改才行
Webpack React/Redux Hot Module Reloading (HMR) example

sagaManager.js 中修改以下內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
const CANCEL_SAGAS_HMR = 'CANCEL_SAGAS_HMR';
function createAbortableSaga(saga) {
if (process.env.NODE_ENV === 'development') {
return function* main() {
const sagaTask = yield fork(saga);
yield take(CANCEL_SAGAS_HMR);
yield cancel(sagaTask);
};
}
return saga;
}
export default {
runSagas(sagaMiddleware) {
rootSaga.map(createAbortableSaga).forEach((saga) => sagaMiddleware.run(saga));
},
cancelSagas(store) {
store.dispatch({ type: CANCEL_SAGAS_HMR });
}
};

store 加入 Saga 的 Hot-reload

1
2
3
4
5
6
7
8
9
10
11
...
if (__DEVELOPMENT__ && module.hot) {
module.hot.accept('./sagaManager', () => {
sagaManager.cancelSagas(store);
const nextSagas = require('./sagaManager');
nextSagas.runSagas(sagaMiddleware);
});
}
...

參考

這篇概略的介紹 redux-saga 的特性和常用到的 Effects,以及怎麼在專案中套用,如果想進一步了解更多可以參考以下連結
github
gitbook