Redux Toolkit createSlice: Diagnosing State That Does Not Update
Dispatch a createSlice action and see no state change? Use this diagnostic guide to find whether the cause is a stale reference, direct mutation outside Immer, async logic in a reducer, or non-serializable data.
21 Oct 2025, 04:28 UTC

Recognizable condition
You dispatch an action created by Redux Toolkit's createSlice. Redux DevTools shows the action was received, but the state tree looks identical before and after. Components wrapped in Provider and using useSelector do not re-render. The reducer appears to run, yet nothing changes.
Useful takeaway
Most silent stalls in createSlice trace back to one of four causes: the reducer returns the original state reference, state is mutated outside an Immer draft, asynchronous logic lives inside a slice reducer, or the state contains non-serializable values. Check the state reference and reducer return value before rewriting feature logic.
Cause and diagnostic table
| Symptom | Likely cause | First check |
|---|---|---|
| Action appears in DevTools; state object is unchanged | Reducer returned the incoming state reference instead of a new object | Compare previous and next state references in DevTools |
| State updates in DevTools, but component does not re-render | Selector returns the same reference, or state mutated outside Immer | Inspect whether the selector result changed between renders |
| Action logged, but no state diff appears | Async code such as async, await, or fetch placed directly in a slice case reducer | Open the slice source and look for asynchronous operations inside reducers |
| Persistence fails or state loses values after reload | Non-serializable value such as Date, function, or class instance stored in Redux state | Run JSON.stringify on the root state and watch for thrown errors or undefined values |
Ordered checks
- Confirm the slice is wired into the store. Open the file where you call
configureStore. Verify the slice reducer is listed under thereducerobject with a unique key. - Check whether the reducer returns a new state. Inside
createSlice, Immer lets you mutate a draft, but the final result must differ from the incomingstate. If a case reducer ends without mutating the draft or returning a value, Redux may reuse the previous state. - Look for direct mutation outside an Immer draft.
createSlicewraps case reducers in Immer, sostate.value += 1is safe inside the reducer. Assigning to a global variable or mutating an argument outside the draft is not. - Search for asynchronous logic in slice reducers. Slice reducers must be synchronous. If you see
async,await,fetch, or promise chains insidereducers, that logic will not run as intended. - Test serializability. Redux state must survive conversion to JSON. Functions,
Dateobjects, class instances, and Immer drafts cannot be persisted or time-traveled reliably.
Fixes tied to findings
Fix 1: Return a new state reference
If your reducer returns the original state object, React Redux sees no reference change and skips re-renders. Use Immer's draft to mutate safely:
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) {
// Safe: Immer records the draft mutation and produces a new state.
state.value += 1;
},
},
});
export const { increment } = counterSlice.actions;
Do not write a plain reducer that reassigns the parameter and returns it, unless you are intentionally returning a new object:
// Risky outside Immer: reassignment does not mutate the draft Redux tracks.
reducers: {
badIncrement(state) {
state = { value: state.value + 1 };
},
},
Fix 2: Move asynchronous logic out of the slice
createSlice does not manage side effects. If you need to fetch data, use createAsyncThunk and handle the lifecycle actions in extraReducers:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchData = createAsyncThunk('data/fetch', async () => {
const response = await fetch('/api/data');
return response.json();
});
const dataSlice = createSlice({
name: 'data',
initialState: { items: [], status: 'idle' },
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchData.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchData.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
});
},
});
Fix 3: Keep state serializable
Before storing data in Redux, convert Date objects to ISO strings and remove functions or class instances:
// Safe for Redux state
{ createdAt: new Date().toISOString() }
// Unsafe for Redux state
{ createdAt: new Date(), log: () => {} }
Verification
- Run
npm ls @reduxjs/toolkitin the project directory to confirm the package is installed. No special permissions are required for read-only inspection. - Write a unit test that dispatches a slice action and asserts the next state is not the same reference as the previous state:
import { counterSlice } from './counterSlice';
const initial = { value: 0 };
const next = counterSlice.reducer(initial, counterSlice.actions.increment());
console.assert(next !== initial, 'State reference must change');
console.assert(next.value === 1, 'Value must increment');
Expected check: both assertions pass without output. Risk: mutating initial inside the test would break the assertion and reveal an Immer configuration problem.
- Open Redux DevTools in the browser, dispatch the action, and confirm a state diff appears. If the action is listed but the state tree does not change, return to Check 2.
- Run
JSON.stringify(store.getState())in a browser console or test. If it throws or producesundefinedfor a field, remove the non-serializable value before persisting state tolocalStorageor a server.
Limitations
createSlicegenerates action types and creators automatically, which can make action names harder to grep in large codebases.- Immer proxies add a small runtime cost. For extremely large state trees, measure whether the overhead matters.
- This guide assumes Redux Toolkit 2.x and React Redux 8+. Older versions using legacy
createStoremay require migration before these patterns apply. - This is a diagnostic draft. Verify behavior in your own environment; no command output in this article was captured from a live system.
Escalation criteria
Escalate beyond the slice when:
- The store state updates correctly, but components still do not re-render. The issue is likely in
useSelectorreference equality,Providerplacement, or React rendering logic, notcreateSlice. - Multiple slices write to overlapping state keys and overwrite each other. Check
configureStorereducer keys for collisions. - You need complex side effects such as cancellation, retries, or sequential dispatches. Consider middleware such as redux-thunk or redux-saga instead of adding logic to slice reducers.
Rollback
If you edit a reducer and the application behaves worse, revert the changed file from version control before escalating. Because this guide modifies application code, restoring the previous commit returns the store to its prior behavior without requiring a database migration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.