跳到主要內容

createListenerMiddleware

概觀

Redux middleware,讓您可以定義包含「效果」回呼的「監聽器」項目,以及根據已發送的動作或狀態變更指定該回呼應執行時間的方式。

它旨在成為更廣泛使用的 Redux 非同步中間件(例如 sagas 和 observables)的輕量級替代方案。雖然在複雜性和概念上類似於 thunk,但它可用於複製一些常見的 saga 使用模式。

從概念上講,你可以將其視為類似於 React 的 useEffect hook,只不過它會根據 Redux store 更新而不是元件屬性/狀態更新來執行邏輯。

偵聽器效果回呼可以存取 dispatchgetState,類似於 thunk。偵聽器還會接收一組非同步工作流程函式,例如 takeconditionpauseforkunsubscribe,這些函式允許撰寫更複雜的非同步邏輯。

偵聽器可以在設定期間透過呼叫 listenerMiddleware.startListening() 靜態定義,或在執行階段使用特殊的 dispatch(addListener())dispatch(removeListener()) 動作動態新增和移除。

基本用法

import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit'

import todosReducer, {
todoAdded,
todoToggled,
todoDeleted,
} from '../features/todos/todosSlice'

// Create the middleware instance and methods
const listenerMiddleware = createListenerMiddleware()

// Add one or more listener entries that look for specific actions.
// They may contain any sync or async logic, similar to thunks.
listenerMiddleware.startListening({
actionCreator: todoAdded,
effect: async (action, listenerApi) => {
// Run whatever additional side-effect-y logic you want here
console.log('Todo added: ', action.payload.text)

// Can cancel other running instances
listenerApi.cancelActiveListeners()

// Run async logic
const data = await fetchData()

// Pause until action dispatched or state changed
if (await listenerApi.condition(matchSomeAction)) {
// Use the listener API methods to dispatch, get state,
// unsubscribe the listener, start child tasks, and more
listenerApi.dispatch(todoAdded('Buy pet food'))

// Spawn "child tasks" that can do more work and return results
const task = listenerApi.fork(async (forkApi) => {
// Can pause execution
await forkApi.delay(5)
// Complete the child by returning a value
return 42
})

const result = await task.result
// Unwrap the child result in the listener
if (result.status === 'ok') {
// Logs the `42` result value that was returned
console.log('Child succeeded: ', result.value)
}
}
},
})

const store = configureStore({
reducer: {
todos: todosReducer,
},
// Add the listener middleware to the store.
// NOTE: Since this can receive actions with functions inside,
// it should go before the serializability check middleware
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})

createListenerMiddleware

建立中間件的執行個體,然後應該透過 configureStoremiddleware 參數將其新增到 store。

const createListenerMiddleware = (options?: CreateMiddlewareOptions) =>
ListenerMiddlewareInstance

interface CreateListenerMiddlewareOptions<ExtraArgument = unknown> {
extra?: ExtraArgument
onError?: ListenerErrorHandler
}

type ListenerErrorHandler = (
error: unknown,
errorInfo: ListenerErrorInfo,
) => void

interface ListenerErrorInfo {
raisedBy: 'effect' | 'predicate'
}

中間件選項

  • extra:將注入到每個偵聽器的 listenerApi 參數中的選用「額外引數」。等同於 Redux Thunk 中間件中的「額外引數」
  • onError:選用的錯誤處理常式,它會在 listener 引發同步和非同步錯誤以及 predicate 引發同步錯誤時被呼叫。

偵聽器中間件執行個體

createListenerMiddleware 回傳的「偵聽器中間件執行個體」是一個類似於 createSlice 所產生「區段」物件的物件。執行個體物件不是實際的 Redux 中間件本身。相反地,它包含中間件和一些用於在中間件中新增和移除偵聽器條目的執行個體方法。

interface ListenerMiddlewareInstance<
State = unknown,
Dispatch extends ThunkDispatch<State, unknown, UnknownAction> = ThunkDispatch<
State,
unknown,
UnknownAction
>,
ExtraArgument = unknown,
> {
middleware: ListenerMiddleware<State, Dispatch, ExtraArgument>
startListening: (options: AddListenerOptions) => Unsubscribe
stopListening: (
options: AddListenerOptions & UnsubscribeListenerOptions,
) => boolean
clearListeners: () => void
}

middleware

實際的 Redux 中介軟體。透過 configureStore.middleware 選項 將其新增到 Redux 儲存。

由於監聽器中介軟體可以接收包含函式的「新增」和「移除」動作,因此通常應將其新增為鏈中的第一個中介軟體,以便在可序列化檢查中介軟體之前。

const store = configureStore({
reducer: {
todos: todosReducer,
},
// Add the listener middleware to the store.
// NOTE: Since this can receive actions with functions inside,
// it should go before the serializability check middleware
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})

startListening

將新的監聽器項目新增到中介軟體。通常用於在應用程式設定期間「靜態」新增新的監聽器。

const startListening = (options: AddListenerOptions) => UnsubscribeListener

interface AddListenerOptions {
// Four options for deciding when the listener will run:

// 1) Exact action type string match
type?: string

// 2) Exact action type match based on the RTK action creator
actionCreator?: ActionCreator

// 3) Match one of many actions using an RTK matcher
matcher?: Matcher

// 4) Return true based on a combination of action + state
predicate?: ListenerPredicate

// The actual callback to run when the action is matched
effect: (action: Action, listenerApi: ListenerApi) => void | Promise<void>
}

type ListenerPredicate<Action extends ReduxAction, State> = (
action: Action,
currentState?: State,
originalState?: State,
) => boolean

type UnsubscribeListener = (
unsubscribeOptions?: UnsubscribeListenerOptions,
) => void

interface UnsubscribeListenerOptions {
cancelActive?: true
}

您必須提供四個選項中的一個來決定何時執行監聽器:typeactionCreatormatcherpredicate。每次發送動作時,都會檢查每個監聽器,以查看它是否應根據提供的當前動作與比較選項執行。

這些都是可以接受的

// 1) Action type string
listenerMiddleware.startListening({ type: 'todos/todoAdded', effect })
// 2) RTK action creator
listenerMiddleware.startListening({ actionCreator: todoAdded, effect })
// 3) RTK matcher function
listenerMiddleware.startListening({
matcher: isAnyOf(todoAdded, todoToggled),
effect,
})
// 4) Listener predicate
listenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
// return true when the listener should run
},
effect,
})

請注意,predicate 選項實際上允許僅針對與狀態相關的檢查進行比對,例如「state.x 是否變更」或「state.x 的目前值是否符合某些條件」,與實際動作無關。

RTK 中包含的 「比對器」工具函式 可作為 matcherpredicate 選項。

傳回值是 unsubscribe() 回呼,它會移除此監聽器。預設情況下,取消訂閱不會取消監聽器的任何活動執行個體。但是,您也可以傳入 {cancelActive: true} 來取消正在執行的執行個體。

如果您嘗試新增監聽器項目,但已經存在具有此確切函式參考的另一個項目,則不會新增新的項目,且會傳回現有的 unsubscribe 方法。

effect 回呼會將目前的動作作為其第一個引數接收,以及類似於 createAsyncThunk 中「thunk API」物件的「監聽器 API」物件。

所有監聽器謂詞和回呼都已在根部簡約器處理動作並更新狀態之後進行檢查。listenerApi.getOriginalState() 方法可用於取得在觸發此監聽器的動作處理之前存在的狀態值。

stopListening

移除指定的監聽器項目。

它接受與 startListening() 相同的參數。它透過比較 listener 的函式參考和提供的 actionCreator/matcher/predicate 函式或 type 字串來檢查現有的監聽器項目。

預設情況下,這不會取消任何正在執行的執行個體。不過,您也可以傳入 {cancelActive: true} 來取消正在執行的執行個體。

const stopListening = (
options: AddListenerOptions & UnsubscribeListenerOptions,
) => boolean

interface UnsubscribeListenerOptions {
cancelActive?: true
}

如果已移除監聽器項目,則傳回 true,如果找不到與所提供的輸入相符的訂閱,則傳回 false

// Examples:
// 1) Action type string
listenerMiddleware.stopListening({
type: 'todos/todoAdded',
listener,
cancelActive: true,
})
// 2) RTK action creator
listenerMiddleware.stopListening({ actionCreator: todoAdded, effect })
// 3) RTK matcher function
listenerMiddleware.stopListening({ matcher, effect, cancelActive: true })
// 4) Listener predicate
listenerMiddleware.stopListening({ predicate, effect })

clearListeners

移除所有目前的監聽器項目。它也會取消這些監聽器的所有正在執行的執行個體。

這在測試場景中很有用,在測試場景中,單一中介軟體或儲存體執行個體可能用於多個測試,以及一些應用程式清理情況。

const clearListeners = () => void;

動作建立器

除了直接呼叫監聽器執行個體上的方法來新增和移除監聽器之外,您還可以在執行階段透過傳送特殊的「新增」和「移除」動作來動態新增和移除監聽器。這些動作會從主要的 RTK 套件中匯出為標準的 RTK 產生的動作建立器。

addListener

標準的 RTK 動作建立器,從套件中匯入。傳送這個動作會告訴中介軟體在執行階段動態新增一個新的監聽器。它接受與 startListening() 完全相同的選項

傳送這個動作會從 dispatch 傳回一個 unsubscribe() 回呼。

// Per above, provide `predicate` or any of the other comparison options
const unsubscribe = store.dispatch(addListener({ predicate, effect }))

removeListener

標準的 RTK 動作建立器,從套件中匯入。傳送這個動作會告訴中介軟體在執行階段動態移除一個監聽器。接受與 stopListening() 相同的參數。

預設情況下,這不會取消任何正在執行的執行個體。不過,您也可以傳入 {cancelActive: true} 來取消正在執行的執行個體。

如果已移除監聽器項目,則傳回 true,如果找不到與所提供的輸入相符的訂閱,則傳回 false

const wasRemoved = store.dispatch(
removeListener({ predicate, effect, cancelActive: true }),
)

clearAllListeners

標準的 RTK 動作建立器,從套件中匯入。傳送這個動作會告訴中介軟體移除所有目前的監聽器項目。它也會取消這些監聽器的所有正在執行的執行個體。

store.dispatch(clearAllListeners())

監聽器 API

listenerApi 物件是每個監聽器回呼的第二個參數。它包含幾個工具函式,可以在監聽器的邏輯中的任何地方呼叫。

export interface ListenerEffectAPI<
State,
Dispatch extends ReduxDispatch<UnknownAction>,
ExtraArgument = unknown,
> extends MiddlewareAPI<Dispatch, State> {
// NOTE: MiddlewareAPI contains `dispatch` and `getState` already

/**
* Returns the store state as it existed when the action was originally dispatched, _before_ the reducers ran.
* This function can **only** be invoked **synchronously**, it throws error otherwise.
*/
getOriginalState: () => State
/**
* Removes the listener entry from the middleware and prevent future instances of the listener from running.
* It does **not** cancel any active instances.
*/
unsubscribe(): void
/**
* It will subscribe a listener if it was previously removed, noop otherwise.
*/
subscribe(): void
/**
* Returns a promise that resolves when the input predicate returns `true` or
* rejects if the listener has been cancelled or is completed.
*
* The return value is `true` if the predicate succeeds or `false` if a timeout is provided and expires first.
*/
condition: ConditionFunction<State>
/**
* Returns a promise that resolves when the input predicate returns `true` or
* rejects if the listener has been cancelled or is completed.
*
* The return value is the `[action, currentState, previousState]` combination that the predicate saw as arguments.
*
* The promise resolves to null if a timeout is provided and expires first.
*/
take: TakePattern<State>
/**
* Cancels all other running instances of this same listener except for the one that made this call.
*/
cancelActiveListeners: () => void
/**
* Cancels the listener instance that made this call.
*/
cancel: () => void
/**
* Throws a `TaskAbortError` if this listener has been cancelled
*/
throwIfCancelled: () => void
/**
* An abort signal whose `aborted` property is set to `true`
* if the listener execution is either aborted or completed.
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
*/
signal: AbortSignal
/**
* Returns a promise that resolves after `timeoutMs` or
* rejects if the listener has been cancelled or is completed.
*/
delay(timeoutMs: number): Promise<void>
/**
* Queues in the next microtask the execution of a task.
*/
fork<T>(executor: ForkedTaskExecutor<T>): ForkedTask<T>
/**
* Returns a promise that resolves when `waitFor` resolves or
* rejects if the listener has been cancelled or is completed.
* @param promise
*/
pause<M>(promise: Promise<M>): Promise<M>
extra: ExtraArgument
}

這些可以分為幾個類別。

儲存互動方法

  • dispatch: Dispatch:標準 store.dispatch 方法
  • getState: () => State:標準 store.getState 方法
  • getOriginalState: () => State:傳回儲存狀態,就像在動作最初被傳送時一樣, reducer 執行之前。(注意:此方法只能在初始傳送呼叫堆疊期間同步呼叫,以避免記憶體外洩。非同步呼叫會擲回錯誤。)
  • extra: unknown:作為中間軟體設定的一部分提供的「額外引數」,如果有提供的話

dispatchgetState 與 thunk 中的完全相同。getOriginalState 可用於比較在監聽器啟動前的原始狀態。

extra 可用於在建立時間將值(例如 API 服務層)注入中間軟體,並在這裡存取。

監聽器訂閱管理

  • unsubscribe: () => void:從中間軟體中移除監聽器條目,並防止監聽器的未來執行個體執行。(這不會取消任何主動執行個體。)
  • subscribe: () => void:如果監聽器條目先前已移除,將重新訂閱該條目,或如果目前已訂閱,則不執行任何操作
  • cancelActiveListeners: () => void:取消此相同監聽器的所有其他執行個體,除了執行此呼叫的個體。(只有在使用其中一個取消感知 API(例如 take/cancel/pause/delay)暫停其他執行個體時,取消才會產生有意義的效果 - 請參閱「使用」區段中的「取消和任務管理」以取得更多詳細資訊)
  • cancel: () => void:取消執行此呼叫的此監聽器執行個體。
  • throwIfCancelled: () => void:如果目前的監聽器執行個體已取消,則擲回 TaskAbortError
  • signal: AbortSignalAbortSignal,如果監聽器執行已中止或完成,其 aborted 屬性將設為 true

動態取消訂閱並重新訂閱此監聽器允許更複雜的非同步工作流程,例如透過在監聽器開始時呼叫 listenerApi.unsubscribe() 來避免重複執行執行個體,或呼叫 listenerApi.cancelActiveListeners() 以確保只有最新的執行個體可以完成。

條件式工作流程執行

  • take: (predicate: ListenerPredicate, timeout?: number) => Promise<[Action, State, State] | null>:傳回一個承諾,當 predicate 傳回 true 時,該承諾將會解析。傳回值是 [action, currentState, previousState] 組合,predicate 將其視為引數。如果提供 timeout 且先過期,承諾將解析為 null
  • condition: (predicate: ListenerPredicate, timeout?: number) => Promise<boolean>:類似於 take,但如果 predicate 成功則解析為 true,如果提供 timeout 且先過期則解析為 false。這允許非同步邏輯暫停並等待某些條件發生後再繼續。有關使用方式的詳細資訊,請參閱下方的「撰寫非同步工作流程」。
  • delay: (timeoutMs: number) => Promise<void>:傳回一個取消感知承諾,在逾時後解析,或在逾期前取消時拒絕
  • pause: (promise: Promise<T>) => Promise<T>:接受任何承諾,並傳回一個取消感知承諾,該承諾會使用引數承諾解析,或在解析前取消時拒絕

這些方法提供根據未來派送的動作和狀態變更撰寫條件式邏輯的能力。兩者也接受毫秒為單位的選用 timeout

take 解析為 [action, currentState, previousState] 組合或 null(如果逾時),而 condition 解析為 true(如果成功)或 false(如果逾時)。

take 用於「等待動作並取得其內容」,而 condition 用於檢查,例如 if (await condition(predicate))

這兩個方法都是取消感知的,如果監聽器執行個體在暫停時取消,它們將擲回 TaskAbortError

請注意,takecondition 都只會在派送 下一個動作 後解析。即使它們的 predicate 會為目前狀態傳回 true,它們也不會立即解析。

子任務

  • fork: (executor: (forkApi: ForkApi) => T | Promise<T>) => ForkedTask<T>:啟動一個「子任務」,可用於完成其他工作。接受任何同步或非同步函式作為其引數,並傳回一個 {result, cancel} 物件,可使用該物件檢查子任務的最終狀態和傳回值,或在進行中時取消它。

可以啟動子任務,並等待收集其傳回值。提供的 executor 函式將非同步呼叫,並包含 {pause, delay, signal}forkApi 物件,允許它暫停或檢查取消狀態。它也可以使用監聽器範圍內的 listenerApi

這方面的範例可能是監聽器,它分派一個包含無限迴圈的子任務,該迴圈會聆聽來自伺服器的事件。然後,父代使用 listenerApi.condition() 等待「停止」動作,並取消子任務。

任務和結果類型為

interface ForkedTaskAPI {
pause<W>(waitFor: Promise<W>): Promise<W>
delay(timeoutMs: number): Promise<void>
signal: AbortSignal
}

export type TaskResolved<T> = {
readonly status: 'ok'
readonly value: T
}

export type TaskRejected = {
readonly status: 'rejected'
readonly error: unknown
}

export type TaskCancelled = {
readonly status: 'cancelled'
readonly error: TaskAbortError
}

export type TaskResult<Value> =
| TaskResolved<Value>
| TaskRejected
| TaskCancelled

export interface ForkedTask<T> {
result: Promise<TaskResult<T>>
cancel(): void
}

TypeScript 用法

中間件程式碼完全是 TS 型別。然而,startListeningaddListener 函式預設不知道儲存的 RootState 型別是什麼,因此 getState() 會傳回 unknown

為了修正這個問題,中間件提供型別來定義這些方法的「預先型別」版本,類似於用來定義預先型別 React-Redux 鉤子的模式。我們特別建議在與實際 configureStore() 呼叫分開的檔案中建立中間件實例

// listenerMiddleware.ts
import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'
import type { RootState, AppDispatch } from './store'

export const listenerMiddleware = createListenerMiddleware()

export const startAppListening = listenerMiddleware.startListening.withTypes<
RootState,
AppDispatch
>()

export const addAppListener = addListener.withTypes<RootState, AppDispatch>()

然後在元件中匯入並使用那些預先型別的方法。

使用指南

整體目的

這個中間件讓您在執行某些動作時執行額外的邏輯,作為 sagas 和 observables 等中間件的輕量級替代方案,它們既有龐大的執行時間套件成本,又有龐大的概念性開銷。

這個中間件並非旨在處理所有可能的用例。就像 thunk 一樣,它為您提供一組基本的原語(包括存取 dispatchgetState),並讓您自由撰寫任何您想要的同步或非同步邏輯。這既是優點(您可以做任何事!),也是缺點(您可以做任何事,沒有任何防護措施!)。

中間件包含幾個非同步工作流程原語,足以撰寫等同於許多 Redux-Saga 效果運算子的效果,例如 takeLatesttakeLeadingdebounce,儘管沒有直接包含這些方法。(請參閱 listener 中間件測試檔案,以取得撰寫等同於這些效果的程式碼範例。)

標準使用模式

最常見的預期用法是「在執行某個動作後執行一些邏輯」。例如,您可以透過尋找特定動作並將萃取的資料傳送至伺服器,包括從儲存中提取使用者詳細資料,來設定一個簡單的分析追蹤器

listenerMiddleware.startListening({
matcher: isAnyOf(action1, action2, action3),
effect: (action, listenerApi) => {
const user = selectUserDetails(listenerApi.getState())

const { specialData } = action.meta

analyticsApi.trackUsage(action.type, user, specialData)
},
})

然而,predicate 選項也允許在某些狀態值變更時,或當狀態符合特定條件時觸發邏輯

listenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
// Trigger logic whenever this field changes
return currentState.counter.value !== previousState.counter.value
},
effect,
})

listenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
// Trigger logic after every action if this condition is true
return currentState.counter.value > 3
},
effect,
})

您也可以實作一個通用 API 擷取功能,其中 UI 會傳送一個描述要請求資源類型的純粹動作,而 middleware 會自動擷取它並傳送結果動作

listenerMiddleware.startListening({
actionCreator: resourceRequested,
effect: async (action, listenerApi) => {
const { name, args } = action.payload
listenerApi.dispatch(resourceLoading())

const res = await serverApi.fetch(`/api/${name}`, ...args)
listenerApi.dispatch(resourceLoaded(res.data))
},
})

(話雖如此,我們建議對任何有意義的資料擷取行為使用 RTK Query - 這主要是您可以在監聽器中執行的範例。)

listenerApi.unsubscribe 方法可以在任何時候使用,且會移除監聽器,使其不再處理任何未來的動作。舉例來說,您可以透過在主體中無條件呼叫 unsubscribe() 來建立一個一次性監聽器 - 效果回呼會在第一次看到相關動作時執行,然後立即取消訂閱且不再執行。(middleware 實際上會在內部對 take/condition 方法使用此技術)

撰寫有條件的非同步工作流程

sagas 和 observables 的一大優點是它們支援複雜的非同步工作流程,包括根據特定傳送動作來停止和啟動行為。然而,缺點是兩者都需要精通具有許多獨特運算子的複雜 API (sagas 的效果方法,例如 call()fork(),observables 的 RxJS 運算子),而且兩者都會大幅增加應用程式套件大小。

儘管監聽器 middleware並非旨在完全取代 sagas 或 observables,但它確實提供了一組經過仔細挑選的 API,也可以實作長時間執行的非同步工作流程。

監聽器可以在 listenerApi 中使用 conditiontake 方法來等待傳送某些動作或滿足狀態檢查。condition 方法直接受到 Temporal.io 工作流程 API 中的 condition 函數 的啟發 (感謝 @swyx 的建議!),而 take 則受到 Redux-Saga 的 take 效果 的啟發。

簽章為

type ConditionFunction<Action extends ReduxAction, State> = (
predicate: ListenerPredicate<Action, State> | (() => boolean),
timeout?: number,
) => Promise<boolean>

type TakeFunction<Action extends ReduxAction, State> = (
predicate: ListenerPredicate<Action, State> | (() => boolean),
timeout?: number,
) => Promise<[Action, State, State] | null>

您可以使用 await condition(somePredicate) 作為暫停監聽器回呼執行的途徑,直到滿足某些條件。

在每個動作都由 reducer 處理後,會呼叫 predicate,且當條件應解析時,它應傳回 true。(它本身實際上是一個一次性監聽器。)如果提供了 timeout 數字 (以毫秒為單位),則如果 predicate 先傳回,承諾會解析為 true,如果逾時,則解析為 false。這讓您可以撰寫類似 if (await condition(predicate, timeout)) 的比較。

這應該能讓您撰寫具有更複雜非同步邏輯的較長時間執行的工作流程,例如 Redux-Saga 的「可取消計數器」範例

來自測試套件的 condition 使用範例

test('condition method resolves promise when there is a timeout', async () => {
let finalCount = 0
let listenerStarted = false

listenerMiddleware.startListening({
predicate: (action, currentState: CounterState) => {
return increment.match(action) && currentState.value === 0
},
effect: async (action, listenerApi) => {
listenerStarted = true
// Wait for either the counter to hit 3, or 50ms to elapse
const result = await listenerApi.condition(
(action, currentState: CounterState) => {
return currentState.value === 3
},
50,
)

// In this test, we expect the timeout to happen first
expect(result).toBe(false)
// Save the state for comparison outside the listener
const latestState = listenerApi.getState()
finalCount = latestState.value
},
})

store.dispatch(increment())
// The listener should have started right away
expect(listenerStarted).toBe(true)

store.dispatch(increment())

// If we wait 150ms, the condition timeout will expire first
await delay(150)
// Update the state one more time to confirm the listener isn't checking it
store.dispatch(increment())

// Handled the state update before the delay, but not after
expect(finalCount).toBe(2)
})

取消和任務管理

監聽器中介軟體支援取消正在執行的監聽器執行個體、take/condition/pause/delay 函式和「子任務」,其實作基於 AbortController

listenerApi.pause/delay() 函式提供一個取消感知的方式,讓目前的監聽器進入休眠。pause() 接受一個承諾,而 delay 接受一個逾時值。如果監聽器在等待時被取消,會擲回一個 TaskAbortError。此外,takecondition 也支援取消中斷。

listenerApi.cancelActiveListeners() 會取消正在執行的其他現有執行個體,而 listenerApi.cancel() 可用於取消目前的執行個體(這可能對 fork 有用,fork 可能會深入巢狀,而且無法直接擲回一個承諾來中斷效應執行)。listenerAPi.throwIfCancelled() 也可於取消在效應執行其他工作時發生時,用於退出工作流程。

listenerApi.fork() 可用於啟動可以執行其他工作的「子任務」。這些任務可以等待來收集其結果。這的範例可能看起來像

listenerMiddleware.startListening({
actionCreator: increment,
effect: async (action, listenerApi) => {
// Spawn a child task and start it immediately
const task = listenerApi.fork(async (forkApi) => {
// Artificially wait a bit inside the child
await forkApi.delay(5)
// Complete the child by returning a value
return 42
})

const result = await task.result
// Unwrap the child result in the listener
if (result.status === 'ok') {
// Logs the `42` result value that was returned
console.log('Child succeeded: ', result.value)
}
},
})

複雜的非同步工作流程

提供的非同步工作流程原語(cancelActiveListenerscancelunsubscribesubscribetakeconditionpausedelay)可用于實作等同於 Redux-Saga 程式庫中許多更複雜的非同步工作流程功能的行為。這包括 throttledebouncetakeLatesttakeLeadingfork/join 等效應。來自測試套件的一些範例

test('debounce / takeLatest', async () => {
// Repeated calls cancel previous ones, no work performed
// until the specified delay elapses without another call
// NOTE: This is also basically identical to `takeLatest`.
// Ref: https://redux-saga.dev.org.tw/docs/api#debouncems-pattern-saga-args
// Ref: https://redux-saga.dev.org.tw/docs/api#takelatestpattern-saga-args

listenerMiddleware.startListening({
actionCreator: increment,
effect: async (action, listenerApi) => {
// Cancel any in-progress instances of this listener
listenerApi.cancelActiveListeners()

// Delay before starting actual work
await listenerApi.delay(15)

// do work here
},
})
}

test('takeLeading', async () => {
// Starts listener on first action, ignores others until task completes
// Ref: https://redux-saga.dev.org.tw/docs/api#takeleadingpattern-saga-args

listenerMiddleware.startListening({
actionCreator: increment,
effect: async (action, listenerApi) => {
listenerCalls++

// Stop listening for this action
listenerApi.unsubscribe()

// Pretend we're doing expensive work

// Re-enable the listener
listenerApi.subscribe()
},
})
})

test('cancelled', async () => {
// cancelled allows checking if the current task was cancelled
// Ref: https://redux-saga.dev.org.tw/docs/api#cancelled

let canceledAndCaught = false
let canceledCheck = false

// Example of canceling prior instances conditionally and checking cancellation
listenerMiddleware.startListening({
matcher: isAnyOf(increment, decrement, incrementByAmount),
effect: async (action, listenerApi) => {
if (increment.match(action)) {
// Have this branch wait around to be cancelled by the other
try {
await listenerApi.delay(10)
} catch (err) {
// Can check cancellation based on the exception and its reason
if (err instanceof TaskAbortError) {
canceledAndCaught = true
}
}
} else if (incrementByAmount.match(action)) {
// do a non-cancellation-aware wait
await delay(15)
if (listenerApi.signal.aborted) {
canceledCheck = true
}
} else if (decrement.match(action)) {
listenerApi.cancelActiveListeners()
}
},
})
})

一個更實際的範例:這個基於 saga 的「長輪詢」迴圈 重複詢問伺服器取得訊息,然後處理每個回應。當「開始輪詢」動作被發送時,會依需求啟動子迴圈,當「停止輪詢」動作被發送時,會取消迴圈。

此方法可透過監聽器中介軟體實作

// Track how many times each message was processed by the loop
const receivedMessages = {
a: 0,
b: 0,
c: 0,
}

const eventPollingStarted = createAction('serverPolling/started')
const eventPollingStopped = createAction('serverPolling/stopped')

listenerMiddleware.startListening({
actionCreator: eventPollingStarted,
effect: async (action, listenerApi) => {
// Only allow one instance of this listener to run at a time
listenerApi.unsubscribe()

// Start a child job that will infinitely loop receiving messages
const pollingTask = listenerApi.fork(async (forkApi) => {
try {
while (true) {
// Cancellation-aware pause for a new server message
const serverEvent = await forkApi.pause(pollForEvent())
// Process the message. In this case, just count the times we've seen this message.
if (serverEvent.type in receivedMessages) {
receivedMessages[
serverEvent.type as keyof typeof receivedMessages
]++
}
}
} catch (err) {
if (err instanceof TaskAbortError) {
// could do something here to track that the task was cancelled
}
}
})

// Wait for the "stop polling" action
await listenerApi.condition(eventPollingStopped.match)
pollingTask.cancel()
},
})

在元件內新增監聽器

監聽器可在執行階段透過 dispatch(addListener()) 新增。這表示您可以在有權存取 dispatch 的任何地方新增監聽器,包括 React 元件。

由於 addListener 的 dispatch 會傳回一個 unsubscribe 回呼,這自然會對應到 React useEffect 勾子的行為,讓您傳回一個清理函式。您可以在效果中新增一個監聽器,並在勾子清理時移除監聽器。

基本模式可能如下所示

useEffect(() => {
// Could also just `return dispatch(addListener())` directly, but showing this
// as a separate variable to be clear on what's happening
const unsubscribe = dispatch(
addListener({
actionCreator: todoAdded,
effect: (action, listenerApi) => {
// do some useful logic here
},
}),
)
return unsubscribe
}, [])

雖然此模式可行,但我們不一定要建議這麼做!React 和 Redux 社群一直試著強調盡可能根據狀態為基礎來進行行為。讓 React 元件直接連結到 Redux 動作 dispatch 管線可能會導致更難維護的程式碼庫。

同時,這確實是一個有效的技術,無論是在 API 行為或潛在使用案例方面。將 sagas 作為程式碼分割應用程式的一部分進行延遲載入很常見,而且這通常需要一些複雜的額外設定工作來「注入」sagas。相對之下,dispatch(addListener()) 自然而然地符合 React 元件的生命週期。

因此,雖然我們沒有特別鼓勵使用此模式,但值得在此記錄下來,讓使用者知道這是一個可能性。

在檔案中整理監聽器

作為一個起點,最好在一個獨立的檔案中建立監聽器中介軟體,例如 app/listenerMiddleware.ts,而不是與儲存體放在同一個檔案中。這可以避免其他檔案嘗試匯入 middleware.addListener 時出現任何潛在的循環匯入問題。

從這裡開始,到目前為止,我們想出了三種不同的方式來整理監聽器函式和設定。

首先,您可以從區段檔案匯入效果回呼到中介軟體檔案,並新增監聽器

app/listenerMiddleware.ts
import { action1, listener1 } from '../features/feature1/feature1Slice'
import { action2, listener2 } from '../features/feature2/feature2Slice'

listenerMiddleware.startListening({ actionCreator: action1, effect: listener1 })
listenerMiddleware.startListening({ actionCreator: action2, effect: listener2 })

這可能是最簡單的選項,並且反映了儲存體設定如何將所有區段簡約器組合在一起以建立應用程式。

第二個選項相反:讓區段檔案匯入中介軟體並直接新增其監聽器

features/feature1/feature1Slice.ts
import { listenerMiddleware } from '../../app/listenerMiddleware'

const feature1Slice = createSlice(/* */)
const { action1 } = feature1Slice.actions

export default feature1Slice.reducer

listenerMiddleware.startListening({
actionCreator: action1,
effect: () => {},
})

這會將所有邏輯保留在區段中,儘管它確實將設定鎖定在單一中介軟體執行個體中。

第三個選項是在區段中建立一個設定函式,但讓監聽器檔案在啟動時呼叫該函式

features/feature1/feature1Slice.ts
import type { AppStartListening } from '../../app/listenerMiddleware'

const feature1Slice = createSlice(/* */)
const { action1 } = feature1Slice.actions

export default feature1Slice.reducer

export const addFeature1Listeners = (startListening: AppStartListening) => {
startListening({
actionCreator: action1,
effect: () => {},
})
}
app/listenerMiddleware.ts
import { addFeature1Listeners } from '../features/feature1/feature1Slice'

addFeature1Listeners(listenerMiddleware.startListening)

請隨時使用最適合您應用程式的這些方法。