食譜
頻流控管
你可以使用內建的頻流控管ヘルパー throttle
來控管一連串已傳送的動作。例如,假設當使用者正在文字欄位輸入時,UI 會發起一個 INPUT_CHANGED
動作。
import { throttle } from 'redux-saga/effects'
function* handleInput(input) {
// ...
}
function* watchInput() {
yield throttle(500, 'INPUT_CHANGED', handleInput)
}
藉由此ヘルパー,watchInput
將不會在 500 毫秒內為 handleInput
任務啟動新的任務,但同時它仍會接受最新的 INPUT_CHANGED
動作到其底層的 buffer
中,所以它會遺漏在這中間發生的所有 INPUT_CHANGED
動作。這可以確保 Saga 會在 500 毫秒的每一段時間最多只進行一個 INPUT_CHANGED
動作,但還是可以處理尾隨的動作。
去抖動
從 redux-saga@v1 debounce 開始是內建的效果。
讓我們來思考看看如何把這個效果實作為其他基礎效果的組合:
要排除序列,請將內建 delay
輔助程式放入分叉工作中
import { call, cancel, fork, take, delay } from 'redux-saga/effects'
function* handleInput(input) {
// debounce by 500ms
yield delay(500)
...
}
function* watchInput() {
let task
while (true) {
const { input } = yield take('INPUT_CHANGED')
if (task) {
yield cancel(task)
}
task = yield fork(handleInput, input)
}
}
在上方的範例中,handleInput
在執行邏輯之前會等待 500ms。如果使用者在此期間輸入某項內容,我們將會得到更多 INPUT_CHANGED
動作。由於 handleInput
仍會封鎖在 delay
呼叫中,因此會在執行邏輯之前被 watchInput
取消。
上方的範例可以使用 redux-saga takeLatest
輔助程式改寫
import { call, takeLatest, delay } from 'redux-saga/effects'
function* handleInput({ input }) {
// debounce by 500ms
yield delay(500)
...
}
function* watchInput() {
// will cancel current running handleInput task
yield takeLatest('INPUT_CHANGED', handleInput);
}
重試 XHR 呼叫
從 redux-saga@v1 retry 為內建效果。
讓我們來思考看看如何把這個效果實作為其他基礎效果的組合:
要重試 XHR 呼叫指定次數,請使用一個包含延遲的 for 迴圈
import { call, put, take, delay } from 'redux-saga/effects'
function* updateApi(data) {
for (let i = 0; i < 5; i++) {
try {
const apiResponse = yield call(apiRequest, { data })
return apiResponse
} catch (err) {
if (i < 4) {
yield delay(2000)
}
}
}
// attempts failed after 5 attempts
throw new Error('API request failed')
}
export default function* updateResource() {
while (true) {
const { data } = yield take('UPDATE_START')
try {
const apiResponse = yield call(updateApi, data)
yield put({
type: 'UPDATE_SUCCESS',
payload: apiResponse.body,
})
} catch (error) {
yield put({
type: 'UPDATE_ERROR',
error,
})
}
}
}
在上方的範例中,apiRequest
將重試 5 次,每次會間隔 2 秒。在第 5 次失敗後,拋出的例外會被母系 saga 捕獲,而母系 saga 也會傳送 UPDATE_ERROR
動作。
如果想要無限制重試,則可以將 for
迴圈改為 while (true)
。此外,也可以使用 takeLatest
取代 take
,如此一來只有最後一個要求會重試。藉由在錯誤處理中加入 UPDATE_RETRY
動作,我們可以通知使用者更新未成功,但將會重試。
import { delay } from 'redux-saga/effects'
function* updateApi(data) {
while (true) {
try {
const apiResponse = yield call(apiRequest, { data })
return apiResponse
} catch (error) {
yield put({
type: 'UPDATE_RETRY',
error,
})
yield delay(2000)
}
}
}
function* updateResource({ data }) {
const apiResponse = yield call(updateApi, data)
yield put({
type: 'UPDATE_SUCCESS',
payload: apiResponse.body,
})
}
export function* watchUpdateResource() {
yield takeLatest('UPDATE_START', updateResource)
}
復原
復原的能力在假設使用者不知道自己在做什麼之前,就先讓動作順利發生,來讓使用者覺得被尊重 (連結)。 redux 文件 說明了實作復原的強健方式,也就是要修改 reducer,以便包含 past
、present
和 future
狀態。甚至有一個 redux-undo 函式庫,可以產生較高階的 reducer,來協助開發人員執行大部分繁重的工作。
不過,這個方法會帶來負擔,因為它會儲存應用程式過去狀態的參考。
使用 redux-saga 的 delay
和 race
,我們可以實作基礎的一次性復原,無須增強 reducer 或儲存過去的狀態。
import { take, put, call, spawn, race, delay } from 'redux-saga/effects'
import { updateThreadApi, actions } from 'somewhere'
function* onArchive(action) {
const { threadId } = action
const undoId = `UNDO_ARCHIVE_${threadId}`
const thread = { id: threadId, archived: true }
// show undo UI element, and provide a key to communicate
yield put(actions.showUndo(undoId))
// optimistically mark the thread as `archived`
yield put(actions.updateThread(thread))
// allow the user 5 seconds to perform undo.
// after 5 seconds, 'archive' will be the winner of the race-condition
const { undo, archive } = yield race({
undo: take(action => action.type === 'UNDO' && action.undoId === undoId),
archive: delay(5000),
})
// hide undo UI element, the race condition has an answer
yield put(actions.hideUndo(undoId))
if (undo) {
// revert thread to previous state
yield put(actions.updateThread({ id: threadId, archived: false }))
} else if (archive) {
// make the API call to apply the changes remotely
yield call(updateThreadApi, thread)
}
}
function* main() {
while (true) {
// wait for an ARCHIVE_THREAD to happen
const action = yield take('ARCHIVE_THREAD')
// use spawn to execute onArchive in a non-blocking fashion, which also
// prevents cancellation when main saga gets cancelled.
// This helps us in keeping state in sync between server and client
yield spawn(onArchive, action)
}
}
分批動作
redux
不支援傳送多個動作,只呼叫一次 reducer 的功能。這有效能上的影響,而且需要依序傳送多個動作的人體工學也不太好。
相反地,我們可以使用第三方函式庫 redux-batched-actions。這是一個簡易的 reducer 和動作,讓最終開發人員可以傳送多個動作,只呼叫 reducer 一次。
如果您有一個代碼庫需要同時派發許多動作,我們推薦使用此配方。
import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware, { stdChannel } from 'redux-saga';
import { enableBatching, BATCH } from 'redux-batched-actions';
// your root reducer
import { rootReducer } from './reducer';
// your root saga
import { rootSaga } from './saga';
const channel = stdChannel();
const rawPut = channel.put;
channel.put = (action: ActionWithPayload<any>) => {
if (action.type === BATCH) {
action.payload.forEach(rawPut);
return;
}
rawPut(action);
};
const sagaMiddleware = createSagaMiddleware({ channel });
const reducer = enableBatching(rootReducer);
// https://redux-toolkit.dev.org.tw/api/configureStore
const store = configureStore({
reducer: rootReducer,
middleware: [sagaMiddleware],
});
sagaMiddleware.run(rootSaga);