Diagnosing Redux Store Updates That Don’t Reflect Dispatched Actions
A step‑by‑step diagnostic guide for when Redux state appears stale after dispatching actions, covering reducer purity, combineReducers wiring, middleware order, and subscription issues.
15 Dec 2025, 17:09 UTC

Problem: UI Doesn’t Reflect Dispatched Actions
You dispatch an action, but the connected components never re‑render. The root cause is usually a break in the predictable flow: dispatch → middleware → root reducer → new state → subscribers. The most useful takeaway is to treat the flow as a checklist — verify each link before assuming a library bug.
Quick Cause/Diagnostic Table
| Observed Symptom | Likely Cause | First Check |
|---|---|---|
| Action appears in DevTools but state unchanged | Reducer returns the same reference (mutation or missing case) | Log the reducer’s return value |
| Action never shows in DevTools | Middleware swallows or fails to forward the action | Add a logging middleware before others |
| State updates but component doesn’t re‑render | Component not subscribed or connect/useSelector selector returns stale reference | Verify subscription count with store.subscribe |
| Only part of the state tree updates | combineReducers key mismatch or reducer not mounted | Inspect root state shape vs. reducer keys |
Ordered Diagnostic Checks
- Confirm dispatch reaches the store. In the browser console run
store.dispatch({type:'@@DIAG/TEST'})and watch Redux DevTools. If the action is missing, the problem is upstream (e.g., a wrapper that calls a different store instance). - Add a temporary logging middleware. Insert
const logger = store => next => action => { console.log('dispatching', action); return next(action); };as the first middleware. Reload and dispatch the failing action. Absence of the log means the middleware chain is broken. - Verify each reducer receives the action. In every reducer add
console.log('reducer:', action.type)at the top. If a reducer never logs, its slice isn’t wired intocombineReducers. - Check reducer purity and immutability. Ensure every branch returns a new object/array (e.g.,
return {...state, count: state.count+1}). Mutatingstatedirectly returns the same reference, so===checks inconnectthink nothing changed. - Validate
combineReducerskeys. The object passed tocombineReducersmust match the shape you expect ingetState(). A typo (e.g.,countersvscounter) creates an empty slice that never updates. - Confirm subscription callbacks fire. Call
store.subscribe(() => console.log('state changed', store.getState()))manually. If it never logs, the store instance used by the UI differs from the one you’re debugging.
Fixes Mapped to Findings
- Reducer returns same reference → Refactor to immutable updates (spread operator,
Array.map,Immerif preferred). - Middleware swallows action → Ensure every middleware calls
next(action)or returns a promise that resolves. Remove or reorder offending middleware. - Missing reducer slice → Add the reducer to the
combineReducersmap with the exact key used in selectors. - Component not subscribed → Check that
connectoruseSelectorreceives the samestoreinstance (common when using multipleProviders). - Selector returns new object each call → Memoise with
reselector ensure selector returns a stable reference for unchanged data.
Escalation Criteria
Escalate to a senior engineer or open a library issue only after:
- All six checks above pass but the UI still doesn’t update.
- Redux DevTools shows the action and a new state object, yet
store.getState() === previousStatereturnstrue. - You have verified a single store instance across the app (no duplicate
createStorecalls).
Concrete Example: Counter with combineReducers
// store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const counter = (state = { value: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, value: state.value + 1 };
case 'DECREMENT':
return { ...state, value: state.value - 1 };
default:
return state;
}
};
const rootReducer = combineReducers({ counter });
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
// component.jsx
import { useSelector, useDispatch } from 'react-redux';
export const Counter = () => {
const value = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({type:'INCREMENT'})}>+</button>
<button onClick={() => dispatch({type:'DECREMENT'})}>-</button>
</div>
);
};
If the buttons do nothing, run the diagnostic checks. A common mistake is writing return state.value + 1 (primitive) instead of a new object, which makes combineReducers think the slice didn’t change.
Verification Steps
- Open Redux DevTools (browser extension) and dispatch
INCREMENT. The action log should show the action and a newcounter.value. - Run unit test:
npm test -- --testPathPattern=counter(requires Jest). The test should dispatchINCREMENTand assertstore.getState().counter.value === 1. - In the console, execute
store.subscribe(() => console.log('sub', store.getState()))and click the button. You should see a log with the updated state.
Limitations
This guide assumes a single‑store Redux setup with combineReducers and standard middleware. It does not cover advanced patterns such as redux-toolkit configureStore auto‑middleware, code‑splitting with dynamic reducers, or server‑side rendering hydration mismatches. For those scenarios, apply the same checklist but also verify reducer injection timing and hydration state equality.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.