store.subscribe fires on every action and tells you nothing about which one. The listener middleware is the targeted version, and Redux Toolkit 3,824 's answer to redux-saga: effects that run only for the actions they name, after the reducer, with an API for cancellation, debouncing and waiting.
const cart = createSlice({ name: 'cart', initialState: 0, reducers: { added: s => s + 1 } });
const listener = createListenerMiddleware();
listener.startListening({
actionCreator: cart.actions.added,
effect: async (action, api) => {
console.log('fired for', action.type, '- state was', api.getOriginalState().cart);
api.cancelActiveListeners(); // drop the run started by the previous action
await api.delay(50); // debounce: only the last run survives
console.log('saving cart, items =', api.getState().cart);
},
});
const store = configureStore({ reducer: { cart: cart.reducer },
middleware: g => g().prepend(listener.middleware) });
for (let i = 0; i < 3; i += 1) store.dispatch(cart.actions.added());fired for cart/added - state was 0 fired for cart/added - state was 1 fired for cart/added - state was 2 saving cart, items = 3
Three dispatches, three effect runs, one save: cancelActiveListeners() aborts the earlier runs, parked in delay, so only the last reaches the network. Writing that with useEffect and a timer ref would need a component that has no business knowing about persistence.
Pick the trigger with exactly one of four keys: type, actionCreator (the usual choice, and typed), matcher (isAnyOf(a, b) or a thunk matcher from createAsyncThunk), or predicate, which compares the previous and current state instead of looking at the action.