This episode covers debugging Redux with Redux DevTools: time-travel debugging, action traces, state diffs, and jump-to-state, then how to troubleshoot common problems like selectors creating new objects, mutation outside reducers, stale state, and hydration errors.

Redux's best-known advantage is debuggability: every state change can be traced, compared, and replayed. Episode 19 teaches how to use Redux DevTools to its full extent, then closes with troubleshooting the problems Redux developers most often encounter in the field.
Redux Toolkit wires up Redux DevTools automatically through configureStore. This tool isn't just for viewing state — with time-travel, action traces, and state diffs, you can answer three key debugging questions: when the state changed, what caused it, and everything else that changed along with it.
DevTools stores the entire action history. The jump buttons rewind state to the point before an action ran:
npm ls @reduxjs/toolkit react-reduxPractical steps:
Time-travel is extremely useful for reproducing bugs: you don't need to guess the sequence of interactions that triggered the error — just rewind and repeat.
The Trace tab shows the stack trace where the action originated: which function called dispatch. The Diff tab shows the state differences before and after an action:
{
"changeType": "UPDATE",
"stateAfter": {
"auth": { "user": { "id": 3, "name": "Budi" } },
"posts": { "status": "succeeded", "items": [ { "id": 1 } ] }
}
}Diff helps find unexpected changes — for example an action that should only change one field but actually changes many. Trace narrows the search down to the code that did it.
The jump buttons let you inspect the state at any point without repeating interactions. It's the fastest way to prove a hypothesis: "was the bug already present at state X?" Compare the healthy state with the problematic one, then find the action that last changed the relevant field.
Symptom: a component renders continuously, or React refuses to render because of an update loop. The most common cause: the selector returns a new object/array on every call.
const user = useAppSelector((state) => ({
name: state.user.name,
role: state.user.role,
}))Every action creates a new { name, role } object, so useSelector always detects a change. Fix: use shallowEqual or build a memoized selector with createSelector:
import { shallowEqual } from "react-redux"
const user = useAppSelector(
(state) => ({
name: state.user.name,
role: state.user.role,
}),
shallowEqual,
)A simple rule: return primitive values from selectors, or use shallowEqual for partially-read objects and arrays.
Redux Toolkit uses Immer, which detects mutations. Mutating outside a reducer produces an error:
Cannot produce a draft with an immutable valueCommon mistakes: modifying state obtained from getState() inside a thunk, or saving a state reference to a variable and mutating it somewhere else. Fix: all changes happen only inside reducers (or via updateQueryData for the RTK Query cache):
const users = store.getState().users.items
const updated = users.map((u) =>
u.id === 3 ? { ...u, role: "editor" } : u,
)
store.dispatch(usersLoaded(updated))map produces a new array without touching the original state. Immer then handles the draft update inside the reducer.
The classic problem: a callback (such as an event handler or setInterval) captures an old state value through a closure:
const token = store.getState().auth.token
setTimeout(() => {
fetch("/api/data", {
headers: { Authorization: `Bearer ${token}` },
})
}, 5000)If a login happens within those 5 seconds, token is still the old value. Fix: read the state at the moment it's needed with getState:
setTimeout(() => {
const token = store.getState().auth.token
fetch("/api/data", {
headers: { Authorization: `Bearer ${token}` },
})
}, 5000)This pattern also applies to listener middleware — use listenerApi.getState() instead of capturing a value earlier.
Hydration errors occur when the server HTML and the first client render don't match. Common sources: dates or random values computed during render, and state fetched twice with different results:
export default function Clock() {
const [now, setNow] = useState<string | null>(null)
useEffect(() => setNow(new Date().toISOString()), [])
if (!now) return null
return <time>{now}</time>
}Content that depends on runtime values is moved into useEffect so the server renders the same placeholder as the client. For state that must match exactly, hydrate it from the server as in episode 14.
Tip
Enable the trace option in DevTools when you want to trace where an action came from. In production, don't enable it — tracing the stack trace of every action adds unnecessary overhead.
Redux DevTools is a debugging superpower: time-travel replays events, the action trace finds where a change originated, and the state diff shows the scope of a change. Combined with an understanding of the common causes — selectors creating new objects, mutations outside reducers, stale closures, and hydration mismatch — you can narrow down bugs far faster than reading random logs.
Key takeaways:
dispatch was called.shallowEqual.getState when needed to avoid stale closures.In the next episode, episode 20 covers the latest stable RTK 2.x features — you'll explore ESM and TypeScript support, Immer 10 with performance improvements, skills files for AI tooling, and the v2.11 to v2.12.0 release notes from 2026.