Listener Middleware

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.

A debounced save, driven by an actionJavaScript
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());
Output
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.