跳至主要內容

食譜

頻流控管

你可以使用內建的頻流控管ヘルパー 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,以便包含 pastpresentfuture 狀態。甚至有一個 redux-undo 函式庫,可以產生較高階的 reducer,來協助開發人員執行大部分繁重的工作。

不過,這個方法會帶來負擔,因為它會儲存應用程式過去狀態的參考。

使用 redux-saga 的 delayrace,我們可以實作基礎的一次性復原,無須增強 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);