createListenerMiddleware function signature
createListenerMiddleware is a function that takes optional CreateMiddlewareOptions and returns a ListenerMiddlewareInstance. The options parameter is optional and typed as CreateMiddlewareOptions<ExtraArgument = unknown>. The function signature is: const createListenerMiddleware = (options?: CreateMiddlewareOptions) => ListenerMiddlewareInstance.
CreateMiddlewareOptions interface
CreateMiddlewareOptions interface has two optional properties: extra (type ExtraArgument = unknown) which is an optional extra argument injected into the listenerApi parameter of each listener, equivalent to the extra argument in Redux Thunk middleware; and onError (type ListenerErrorHandler) which is an optional error handler that gets called with synchronous and async errors raised by listener and synchronous errors thrown by predicate. ListenerErrorHandler is a function with signature (error: unknown, errorInfo: ListenerErrorInfo) => void. ListenerErrorInfo interface has one property: raisedBy which is a literal type 'effect' | 'predicate'.
ListenerMiddlewareInstance interface properties
ListenerMiddlewareInstance is a generic interface with type parameters State = unknown, Dispatch extends ThunkDispatch<State, unknown, UnknownAction> = ThunkDispatch<State, unknown, UnknownAction>, and ExtraArgument = unknown. It has four properties: middleware (type ListenerMiddleware<State, Dispatch, ExtraArgument>) which is the actual Redux middleware to add to the store; startListening (type function (options: AddListenerOptions) => Unsubscribe) which adds a new listener entry; stopListening (type function (options: AddListenerOptions & UnsubscribeListenerOptions) => boolean) which removes a listener entry; and clearListeners (type function () => void) which removes all listener entries.
startListening method signature and parameters
startListening accepts options of type AddListenerOptions and returns type UnsubscribeListener. AddListenerOptions interface requires exactly one of four action matching options: type (string for exact action type match), actionCreator (ActionCreator for exact match based on RTK action creator), matcher (Matcher for RTK matcher function), or predicate (ListenerPredicate for custom predicate logic). All four options are optional but one must be provided. The interface also requires effect (type (action: Action, listenerApi: ListenerApi) => void | Promise<void>) which is the callback to run when the action is matched. ListenerPredicate<Action extends ReduxAction, State> is a function with signature (action: Action, currentState?: State, originalState?: State) => boolean. UnsubscribeListener is a function with signature (unsubscribeOptions?: UnsubscribeListenerOptions) => void. UnsubscribeListenerOptions has one optional property: cancelActive (type true) which cancels active instances when unsubscribing.
stopListening method signature and return value
stopListening accepts options of type AddListenerOptions & UnsubscribeListenerOptions and returns a boolean. It removes a given listener entry by comparing function references of the listener and provided actionCreator/matcher/predicate function or type string. By default it does not cancel any active running instances, but passing {cancelActive: true} will cancel running instances. Returns true if the listener entry was removed, or false if no subscription matching the input was found.
clearListeners method signature
clearListeners is a method with signature () => void. It removes all current listener entries and cancels all active running instances of those listeners. It is most useful for test scenarios or app cleanup situations.
addListener action creator
addListener is a standard RTK action creator exported from the package. Dispatching this action tells the middleware to dynamically add a new listener at runtime. It accepts exactly the same options as startListening(). Dispatching this action returns an unsubscribe() callback from dispatch, allowing runtime listener management.
removeListener action creator
removeListener is a standard RTK action creator exported from the package. Dispatching this action tells the middleware to dynamically remove a listener at runtime. It accepts the same arguments as stopListening(). By default it does not cancel any active running instances, but passing {cancelActive: true} cancels running instances. Returns true if the listener entry was removed, or false if no subscription matching the input was found.
clearAllListeners action creator
clearAllListeners is a standard RTK action creator exported from the package. Dispatching this action tells the middleware to remove all current listener entries and cancel all active running instances of those listeners.
take and condition methods only resolve after next action
Both take and condition methods will only resolve after the next action has been dispatched. They do not resolve immediately even if their predicate would return true for the current state at the time they are called.
ListenerEffectAPI interface methods and properties
ListenerEffectAPI<State, Dispatch extends ReduxDispatch<UnknownAction>, ExtraArgument = unknown> extends MiddlewareAPI<Dispatch, State> and includes the following properties and methods: dispatch (inherited from MiddlewareAPI), getState (inherited from MiddlewareAPI), getOriginalState () => State (returns store state before reducers ran, synchronous only), unsubscribe () => void (removes listener, does not cancel active instances), subscribe () => void (re-subscribes previously removed listener or no-op), condition (ConditionFunction type, returns Promise<boolean> resolving when predicate returns true or false on timeout), take (TakePattern type, returns Promise<[Action, State, State] | null> resolving with action tuple or null on timeout), cancelActiveListeners () => void (cancels all other running instances except the caller), cancel () => void (cancels the current listener instance), throwIfCancelled () => void (throws TaskAbortError if listener was cancelled), signal (AbortSignal with aborted property), delay (timeoutMs: number) => Promise<void> (returns cancellation-aware promise), fork<T> (executor: ForkedTaskExecutor<T>) => ForkedTask<T> (launches child task), pause<M> (promise: Promise<M>) => Promise<M> (returns cancellation-aware promise wrapper), and extra (ExtraArgument).
ConditionFunction and TakeFunction signatures
ConditionFunction<Action extends ReduxAction, State> has signature (predicate: ListenerPredicate<Action, State> | (() => boolean), timeout?: number) => Promise<boolean>. It returns true if predicate succeeds or false if timeout (in ms) expires first. TakeFunction<Action extends ReduxAction, State> has signature (predicate: ListenerPredicate<Action, State> | (() => boolean), timeout?: number) => Promise<[Action, State, State] | null>. It resolves to [action, currentState, previousState] tuple or null if timeout expires first.
ForkedTaskAPI interface
ForkedTaskAPI interface has three properties: pause<W>(waitFor: Promise<W>) => Promise<W> which pauses execution waiting for a promise; delay(timeoutMs: number) => Promise<void> which returns a promise resolving after timeout; and signal (AbortSignal) which indicates if task execution is aborted or completed.
TaskResult type and ForkedTask interface
TaskResult<Value> is a union type with three variants: TaskResolved<Value> with readonly status: 'ok' and readonly value: T; TaskRejected with readonly status: 'rejected' and readonly error: unknown; and TaskCancelled with readonly status: 'cancelled' and readonly error: TaskAbortError. ForkedTask<T> interface has two properties: result (Promise<TaskResult<T>>) which resolves with the task result, and cancel() method which cancels the task.
TypeScript pre-typed listener methods pattern
To get proper TypeScript typing for RootState and AppDispatch in startListening and addListener, create pre-typed versions using withTypes method. Create the middleware in a separate file and use withTypes to pass RootState, AppDispatch, and optional ExtraArgument types. Example: export const startAppListening = listenerMiddleware.startListening.withTypes<RootState, AppDispatch, ExtraArgument>(). Similarly for addListener: export const addAppListener = addListener.withTypes<RootState, AppDispatch>(). This follows the same pattern as pre-typed React-Redux hooks.
Basic createListenerMiddleware usage example
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit'
import todosReducer, { todoAdded, todoToggled, todoDeleted } from '../features/todos/todosSlice'
const listenerMiddleware = createListenerMiddleware()
listenerMiddleware.startListening({
actionCreator: todoAdded,
effect: async (action, listenerApi) => {
console.log('Todo added: ', action.payload.text)
listenerApi.cancelActiveListeners()
const data = await fetchData()
if (await listenerApi.condition(matchSomeAction)) {
listenerApi.dispatch(todoAdded('Buy pet food'))
const task = listenerApi.fork(async (forkApi) => {
await forkApi.delay(5)
return 42
})
const result = await task.result
if (result.status === 'ok') {
console.log('Child succeeded: ', result.value)
}
}
},
})
const store = configureStore({
reducer: { todos: todosReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})
Listener middleware must be prepended before serializability check
The listener middleware should be added as the first middleware in the chain using prepend() rather than append(), because the middleware can receive actions with functions inside (add and remove listener actions). It must be placed before the serializability check middleware to avoid false positive errors about unserializable actions.
getOriginalState can only be called synchronously
getOriginalState() returns the store state as it existed when the action was originally dispatched, before reducers ran. This function can only be invoked synchronously during the initial dispatch call stack. Calling it asynchronously will throw an error. This restriction exists to avoid memory leaks.
Predicate and effect callbacks timing
All listener predicates and effect callbacks are checked and run after the root reducer has already processed the action and updated the state. The predicate is evaluated against the new state, not the old state. To access the original state before the action was processed, use listenerApi.getOriginalState().
Exactly one action matching option required
When calling startListening or stopListening, you must provide exactly one of the four action matching options: type, actionCreator, matcher, or predicate. Providing zero or more than one will result in incorrect behavior. Every dispatched action is checked against each listener to determine if it should run based on the comparison option provided.
Predicate option allows state-only matching
The predicate option allows matching solely against state-related checks, independent of the actual action. For example, you can trigger logic when a specific state field changes (by comparing currentState and previousState) or when the state matches particular criteria, regardless of which action was dispatched.
Duplicate listener detection
If you try to add a listener entry but another entry with the exact same function reference for the listener and comparison option (actionCreator/matcher/predicate or type string) already exists, no new entry will be added. Instead, the existing unsubscribe method will be returned.
unsubscribe does not cancel active instances by default
When calling unsubscribe() or removeListener(), active running instances are not cancelled by default. To cancel active instances, pass {cancelActive: true} option to the unsubscribe call.
Listener middleware organizational patterns
There are three patterns for organizing listeners: 1) Import effect callbacks from slice files into the listener middleware file and add listeners there; 2) Have slice files import the middleware and directly add their listeners; 3) Create a setup function in the slice but let the listener file call it on startup. The first pattern is simplest and mirrors store setup. Choose based on preference and app structure.
Listener middleware in separate file best practice
It is best to create the listener middleware in a separate file (such as app/listenerMiddleware.ts) rather than in the same file as the store. This avoids potential circular import problems from other files trying to import middleware.addListener.
Fork executor function signature
fork() accepts an executor function that can be either sync or async and receives a forkApi parameter. The executor has signature ForkedTaskExecutor<T> which is (forkApi: ForkedTaskAPI) => T | Promise<T>. The executor can return a value that will be available in the TaskResult.
createListenerMiddleware provides reactive logic similar to sagas
createListenerMiddleware is a middleware that listens for specific actions and runs effect callbacks in response. It provides a listenerApi object with methods like delay() and dispatch() to handle asynchronous reactive logic as an alternative to redux-saga or redux-observable.
createListenerMiddleware startListening accepts actionCreator or predicate matchers
The startListening function returned from createListenerMiddleware accepts options including an actionCreator property to match a specific generated action creator, or other matching predicates. The effect callback function receives the action and a listenerApi object.
createListenerMiddleware should be prepended to middleware chain
When adding createListenerMiddleware to configureStore, the listener middleware should be prepended before other middleware like thunk or dev checks using the prepend() method on the getDefaultMiddleware result.
createListenerMiddleware setup pattern
Import createListenerMiddleware from '@reduxjs/toolkit'. Create an instance with const listenerMiddleware = createListenerMiddleware(). Add it to configureStore's middleware array using getDefaultMiddleware().prepend(listenerMiddleware.middleware) to ensure it runs before serializability checks. Export a typed startAppListening function by calling listenerMiddleware.startListening.withTypes<RootState, AppDispatch>().