Learn Redux - Debugging & Redux DevTools
Series/Learn Redux/Episode 19
Episode 19 of 23

Learn Redux - Debugging & Redux DevTools

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Redux DevTools: Core Features

Time-Travel Debugging

DevTools stores the entire action history. The jump buttons rewind state to the point before an action ran:

Aktifkan DevTools untuk Redux Toolkit
npm ls @reduxjs/toolkit react-redux

Practical steps:

  • Open the Redux tab in the browser's DevTools extension.
  • Click an action in the left panel to see the state at that moment.
  • Use the replay buttons to re-run the action sequence from the start.
  • Observe how the UI reacts to each step.

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.

Action Trace and State Diff

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:

Contoh state diff
{
  "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.

Jump to State

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.

Common Troubleshooting

Selector Returning New Objects and Infinite Rendering

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.

JSSelector bermasalah
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:

JSPerbaikan dengan shallowEqual
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.

State Mutation Outside Reducers

Redux Toolkit uses Immer, which detects mutations. Mutating outside a reducer produces an error:

Contoh pesan error Immer
Cannot produce a draft with an immutable value

Common 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):

JSPerbaikan: salin sebelum memutasi
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.

Stale State in Callbacks

The classic problem: a callback (such as an event handler or setInterval) captures an old state value through a closure:

JSClosure dengan state basi
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:

JSBaca state saat eksekusi
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 Issues in SSR

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:

JSMenghindari mismatch tanggal
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.

Conclusion

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:

  • DevTools stores the action history for time-travel debugging and jump-to-state.
  • The action trace shows the stack trace of where dispatch was called.
  • The state diff reveals unexpected changes between actions.
  • Selectors returning new objects cause excessive re-renders; use shallowEqual.
  • Immer rejects mutation outside reducers — copy state before mutating.
  • Read state via 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.

Learn Redux - Debugging & Redux DevTools | Learn Redux