From harness-claude
React to Redux actions and state changes with createListenerMiddleware for side effects like analytics, logging, sync, debouncing, and cross-slice coordination. Replaces redux-saga or redux-observable.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> React to dispatched actions and state changes with createListenerMiddleware for structured side effects
Guides Redux and Redux Toolkit for global state management: slices, stores, actions, reducers, hooks, selectors, middleware, async thunks.
Types Redux Toolkit state, actions, thunks, hooks, and middleware with full TypeScript inference and minimal annotations. Use for setup, fixing type errors in slices, extraReducers, or components.
Implements Zustand middleware for persistence (persist), devtools integration, Immer immutability, and custom store enhancements. Guides composition, best practices, partialize, and migrations.
Share bugs, ideas, or general feedback.
React to dispatched actions and state changes with createListenerMiddleware for structured side effects
createListenerMiddleware(). Add it to the store via the middleware callback.startListening to register listeners. Match actions with actionCreator, type, matcher, or predicate.effect callback receives the matched action and a listenerApi with dispatch, getState, getOriginalState, condition, take, delay, and more.listenerApi.condition() to wait for a future state condition before continuing. Use listenerApi.take() to wait for a specific action.listenerApi.cancelActiveListeners() at the start of the effect to debounce — cancels previous runs of the same listener.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),
});
Matching strategies:
actionCreator — exact action creator match (best TypeScript inference)type — string match on action.typematcher — any RTK matcher (isAnyOf, isAllOf, isRejected)predicate — (action, currentState, previousState) => boolean for state-based conditionscondition 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:
Prepend, not concat: Use .prepend(listenerMiddleware.middleware) so listeners run before other middleware.
https://redux-toolkit.js.org/api/createListenerMiddleware