Redux Listener Middleware
React to dispatched actions and state changes with createListenerMiddleware for structured side effects
When to Use
- Running side effects in response to specific actions (analytics, logging, sync)
- Implementing "when X happens, do Y" reactive logic that does not fit in a reducer
- Replacing redux-saga or redux-observable with a simpler built-in alternative
- Coordinating cross-slice logic (when slice A changes, update slice B)
Instructions
- Create the listener middleware once with
createListenerMiddleware(). Add it to the store via the middleware callback.
- Use
startListening to register listeners. Match actions with actionCreator, type, matcher, or predicate.
- The
effect callback receives the matched action and a listenerApi with dispatch, getState, getOriginalState, condition, take, delay, and more.
- Use
listenerApi.condition() to wait for a future state condition before continuing. Use listenerApi.take() to wait for a specific action.
- Use
listenerApi.cancelActiveListeners() at the start of the effect to debounce — cancels previous runs of the same listener.
- Return or call
listenerApi.unsubscribe() to remove the listener dynamically.
// store/listenerMiddleware.ts
import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';
import { addTodo, toggleTodo } from '../features/todos/todos.slice';
import { RootState } from './index';
export const listenerMiddleware = createListenerMiddleware();
// Sync todos to localStorage whenever they change
listenerMiddleware.startListening({
matcher: isAnyOf(addTodo, toggleTodo),
effect: async (action, listenerApi) => {
const state = listenerApi.getState() as RootState;
localStorage.setItem('todos', JSON.stringify(state.todos.items));
},
});
// Debounced search — cancel previous runs
listenerMiddleware.startListening({
actionCreator: setSearchQuery,
effect: async (action, listenerApi) => {
// Cancel any in-progress instances of this listener
listenerApi.cancelActiveListeners();
// Debounce 300ms
await listenerApi.delay(300);
// If we get here, no new setSearchQuery was dispatched
listenerApi.dispatch(fetchSearchResults(action.payload));
},
});
// store/index.ts
import { listenerMiddleware } from './listenerMiddleware';
export const store = configureStore({
reducer: {
/* ... */
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
});
Details
Matching strategies:
actionCreator — exact action creator match (best TypeScript inference)
type — string match on action.type
matcher — any RTK matcher (isAnyOf, isAllOf, isRejected)
predicate — (action, currentState, previousState) => boolean for state-based conditions
condition and take: These let you write multi-step async workflows:
listenerMiddleware.startListening({
actionCreator: startCheckout,
effect: async (action, listenerApi) => {
// Wait for payment to complete (or timeout after 60s)
const [paymentAction] = await listenerApi.take(paymentCompleted.match, 60_000);
if (paymentAction) {
listenerApi.dispatch(finalizeOrder());
} else {
listenerApi.dispatch(checkoutTimedOut());
}
},
});
Comparison with alternatives:
- Thunks: Best for single async operations dispatched from components. Listeners are best for reactive "when X happens do Y" patterns.
- Sagas: Listeners cover most saga use cases without generators. Use sagas only if you need advanced concurrency patterns (races, forks, channels).
- Observables: Listeners handle serial async workflows well. Use RxJS only if you need complex stream composition.
Prepend, not concat: Use .prepend(listenerMiddleware.middleware) so listeners run before other middleware.
Source
https://redux-toolkit.js.org/api/createListenerMiddleware
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.
1---2name: redux-listener-middleware3description: Redux Listener Middleware4---5# Redux Listener Middleware67> React to dispatched actions and state changes with createListenerMiddleware for structured side effects89## When to Use1011- Running side effects in response to specific actions (analytics, logging, sync)12- Implementing "when X happens, do Y" reactive logic that does not fit in a reducer13- Replacing redux-saga or redux-observable with a simpler built-in alternative14- Coordinating cross-slice logic (when slice A changes, update slice B)1516## Instructions17181. Create the listener middleware once with `createListenerMiddleware()`. Add it to the store via the `middleware` callback.192. Use `startListening` to register listeners. Match actions with `actionCreator`, `type`, `matcher`, or `predicate`.203. The `effect` callback receives the matched `action` and a `listenerApi` with `dispatch`, `getState`, `getOriginalState`, `condition`, `take`, `delay`, and more.214. Use `listenerApi.condition()` to wait for a future state condition before continuing. Use `listenerApi.take()` to wait for a specific action.225. Use `listenerApi.cancelActiveListeners()` at the start of the effect to debounce — cancels previous runs of the same listener.236. Return or call `listenerApi.unsubscribe()` to remove the listener dynamically.2425```typescript26// store/listenerMiddleware.ts27import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';28import { addTodo, toggleTodo } from '../features/todos/todos.slice';29import { RootState } from './index';3031export const listenerMiddleware = createListenerMiddleware();3233// Sync todos to localStorage whenever they change34listenerMiddleware.startListening({35 matcher: isAnyOf(addTodo, toggleTodo),36 effect: async (action, listenerApi) => {37 const state = listenerApi.getState() as RootState;38 localStorage.setItem('todos', JSON.stringify(state.todos.items));39 },40});4142// Debounced search — cancel previous runs43listenerMiddleware.startListening({44 actionCreator: setSearchQuery,45 effect: async (action, listenerApi) => {46 // Cancel any in-progress instances of this listener47 listenerApi.cancelActiveListeners();48 // Debounce 300ms49 await listenerApi.delay(300);50 // If we get here, no new setSearchQuery was dispatched51 listenerApi.dispatch(fetchSearchResults(action.payload));52 },53});54```5556```typescript57// store/index.ts58import { listenerMiddleware } from './listenerMiddleware';5960export const store = configureStore({61 reducer: {62 /* ... */63 },64 middleware: (getDefaultMiddleware) =>65 getDefaultMiddleware().prepend(listenerMiddleware.middleware),66});67```6869## Details7071**Matching strategies:**7273- `actionCreator` — exact action creator match (best TypeScript inference)74- `type` — string match on `action.type`75- `matcher` — any RTK matcher (`isAnyOf`, `isAllOf`, `isRejected`)76- `predicate` — `(action, currentState, previousState) => boolean` for state-based conditions7778**condition and take:** These let you write multi-step async workflows:7980```typescript81listenerMiddleware.startListening({82 actionCreator: startCheckout,83 effect: async (action, listenerApi) => {84 // Wait for payment to complete (or timeout after 60s)85 const [paymentAction] = await listenerApi.take(paymentCompleted.match, 60_000);86 if (paymentAction) {87 listenerApi.dispatch(finalizeOrder());88 } else {89 listenerApi.dispatch(checkoutTimedOut());90 }91 },92});93```9495**Comparison with alternatives:**9697- **Thunks:** Best for single async operations dispatched from components. Listeners are best for reactive "when X happens do Y" patterns.98- **Sagas:** Listeners cover most saga use cases without generators. Use sagas only if you need advanced concurrency patterns (races, forks, channels).99- **Observables:** Listeners handle serial async workflows well. Use RxJS only if you need complex stream composition.100101**Prepend, not concat:** Use `.prepend(listenerMiddleware.middleware)` so listeners run before other middleware.102103## Source104105https://redux-toolkit.js.org/api/createListenerMiddleware106107## Process1081091. Read the instructions and examples in this document.1102. Apply the patterns to your implementation, adapting to your specific context.1113. Verify your implementation against the details and edge cases listed above.112113## Harness Integration114115- **Type:** knowledge — this skill is a reference document, not a procedural workflow.116- **No tools or state** — consumed as context by other skills and agents.117118## Success Criteria119120- The patterns described in this document are applied correctly in the implementation.121- Edge cases and anti-patterns listed in this document are avoided.