Skip to main content

πŸ› οΈ Redux DevTools

Redux's superpower is that every state change is a plain, recorded action β€” which means you can inspect, replay, and even rewind your app's entire history. The Redux DevTools browser extension turns that recording into an interactive debugger, and with Redux Toolkit it's wired up for you out of the box.

Week 6 · Day 5 (Friday: Advanced Redux Patterns) · Lecture 3

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Install the Redux DevTools extension and connect a store to it
  • Explain what configureStore enables by default and how to customize devTools
  • Read the action list, state tree, and diff view to understand a change
  • Use time-travel debugging to isolate the exact action that introduced a bug
  • Sanitize secrets with actionSanitizer / stateSanitizer before they reach DevTools
  • Disable or lock down DevTools safely for production builds

Estimated Time: 60 minutes

Practice: Configure a store with sanitizers and a bounded history, then debug a scripted bug.

In This Lesson

What Are Redux DevTools?

The Redux DevTools are a browser extension that plugs into your store and records every action dispatched, the payload it carried, and the resulting state. Because Redux state changes are pure and serializable, the DevTools can do things a normal debugger can't: show you a diff of exactly what changed, let you jump to any point in history, and even re-run a recorded sequence of actions.

🎬 The Film Editing Analogy

Imagine your app is a movie and each action is a single take. The DevTools are the editing suite: every take is logged in order, you can scrub the timeline back to any frame, compare two takes side by side to see what changed, and even splice in a new take to test a scene. Where a plain console log is a single snapshot, DevTools give you the whole reel with a scrubber.

This is the last stop in our advanced-Redux tour. You've normalized state and memoized selectors; now DevTools let you watch that machinery run action by action β€” invaluable when you build the e-commerce cart project next.

Installing & Connecting

First, install the extension for your browser. It adds a "Redux" tab to your browser's developer tools.

  • Chrome / Edge: "Redux DevTools" from the Chrome Web Store (works in Edge too)
  • Firefox: "Redux DevTools" from Firefox Add-ons

The modern way: Redux Toolkit (recommended)

If you're using configureStore β€” which this course does β€” you're already connected. It reads the extension automatically, with zero setup:

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

// DevTools are wired up automatically in development.
export const store = configureStore({
  reducer: rootReducer
});
// Open your browser dev tools β†’ "Redux" tab β†’ dispatch an action β†’ watch it appear.

The legacy way: plain createStore

You'll meet this in older codebases. Without RTK you connect the extension by hand, and the modern helper is composeWithDevTools from the @redux-devtools/extension package:

import { createStore, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk';
import { composeWithDevTools } from '@redux-devtools/extension';
import rootReducer from './reducers';

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

πŸ’‘ Prefer Redux Toolkit

The raw window.__REDUX_DEVTOOLS_EXTENSION__ hookup you'll see in old tutorials still works, but it's error-prone and easy to leave enabled in production. configureStore handles connection, the thunk middleware, and the dev-only guard for you β€” recognize the old pattern, but reach for RTK.

What RTK Enables by Default

Out of the box, configureStore makes a smart, safe choice: DevTools are on in development and off in production. It effectively sets devTools: process.env.NODE_ENV !== 'production' for you. You only pass a devTools option when you want to override that or customize behavior.

import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: rootReducer,
  devTools: {
    name: 'MyApp',        // label this store instance in the extension
    trace: true,          // record a stack trace for each action's origin
    traceLimit: 25,       // how many stack frames to keep
    maxAge: 50,           // keep only the last 50 actions in history
    // Turn individual monitor features on/off:
    features: {
      pause:    true,     // pause recording
      lock:     true,     // lock dispatching
      persist:  true,     // keep state across page reloads
      export:   true,     // export the action log to a file
      import:   'custom',  // import an action log
      jump:     true,     // time-travel jump
      skip:     true,     // skip (cancel) an action
      reorder:  true,     // drag to reorder actions
      dispatch: true,     // dispatch custom actions from the UI
      test:     true      // generate test snippets from actions
    }
  }
});

⚠️ devTools: true does not force production on

Passing devTools: true as a bare boolean overrides the environment guard and will expose your store in production. If you want customization only in dev, gate the whole options object: devTools: isDev ? {...} : false.

The Core Panels

The extension is organized into a handful of panels. In the "Inspector" monitor (the default), the left column is your action list and the right column shows details for the selected action.

graph TD A[Redux DevTools] --> B[Action List] A --> C[Action Payload] A --> D[State Tree] A --> E[Diff View] A --> F[Dispatcher] B --> G[Every dispatch, in order] D --> H[Full state after selected action] E --> I[What changed vs previous action] F --> J[Send custom actions by hand]

Action & state view

Select any action to see its exact shape and the full state it produced. This alone replaces a scatter of console.log calls:

// A selected action, as the Inspector shows it:
{
  type: 'todos/todoAdded',
  payload: { id: '123', text: 'Learn Redux DevTools', completed: false }
}

// …and the resulting normalized state tree:
{
  todos: {
    ids: ['123'],
    entities: { '123': { id: '123', text: 'Learn Redux DevTools', completed: false } }
  },
  ui: { filter: 'all' }
}

The Diff tab

The most-used view: instead of scanning two giant state blobs, the Diff tab highlights only what changed between the previous action and the selected one.

// Before  todos/todoToggled:
{ todos: { entities: { '1': { id: 1, text: 'Learn Redux', completed: false } } } }

// After   todos/todoToggled:
{ todos: { entities: { '1': { id: 1, text: 'Learn Redux', completed: true  } } } }

// Diff tab shows:
//   todos.entities.1.completed:  false β†’ true
//   (everything else marked unchanged)

The Dispatcher

The Dispatcher lets you type an action and fire it into the running store β€” perfect for reproducing a state without clicking through the whole UI:

// Typed straight into the DevTools Dispatcher:
{ type: 'todos/todoAdded', payload: { id: '999', text: 'From DevTools', completed: false } }

Time-Travel Debugging

Because DevTools can re-run your reducers over any prefix of the action log, it can reconstruct the state at any past moment. Click an action (or drag the slider) and your app's UI jumps to how it looked right then. This is time travel, and it's the fastest way to answer "which action broke this?"

A timeline of dispatched actions with a scrubber; jumping back replays reducers up to that action todoAdded filterChanged todosCleared (future) β—€ jumped here Reducers replay from initial state up to the selected action.
Jumping to an action replays your reducers from the initial state up to that point, reconstructing exactly how state β€” and the UI β€” looked.

A typical bug hunt looks like this:

/*
  Bug report: "Todos vanish right after I change the filter."

  1. Reproduce it with the DevTools open.
  2. Read the action list:
       todos/todoAdded
       filter/filterChanged
       todos/todosCleared   ← suspicious! nobody meant to clear todos
  3. Click the action BEFORE todosCleared β€” todos are still there.
  4. Click todosCleared β€” they disappear. That's the culprit.
  5. Open the Diff tab on it to confirm what it wiped.
  6. Fix the reducer that wrongly responded to the filter change.
*/
// The fix: a filter change must NOT touch the todos list.
const todosSlice = createSlice({
  name: 'todos',
  initialState: { ids: [], entities: {} },
  reducers: { /* … */ },
  extraReducers: (builder) => {
    builder.addCase('filter/filterChanged', (state) => {
      // BUG was: return { ids: [], entities: {} };  // ← cleared everything
      return state; // βœ… leave todos untouched on a filter change
    });
  }
});

πŸ’‘ Export & import a session

Found a nasty repro? Use Export to save the whole action log to a JSON file and hand it to a teammate, who can Import it and replay your exact session. It's a bug report that reproduces itself.

Sanitizing Sensitive Data

DevTools record everything β€” including auth tokens, passwords, and payment details if they pass through actions or state. Two hooks let you scrub secrets before they're recorded: actionSanitizer rewrites actions, and stateSanitizer rewrites the state snapshot. Neither changes your real store; they only affect what the extension sees.

const store = configureStore({
  reducer: rootReducer,
  devTools: {
    // Redact secrets carried in action payloads.
    actionSanitizer: (action) => {
      if (action.type === 'auth/loginSuccess') {
        return {
          ...action,
          payload: { ...action.payload, token: '<<REDACTED>>', password: undefined }
        };
      }
      return action;
    },

    // Redact secrets sitting in the state tree.
    stateSanitizer: (state) => {
      if (!state.auth?.token) return state;
      return { ...state, auth: { ...state.auth, token: '<<REDACTED>>' } };
    }
  }
});

The serialize option handles values that aren't plain JSON, like Map and Set, so they display correctly in the inspector instead of as empty objects:

const store = configureStore({
  reducer: rootReducer,
  devTools: {
    serialize: {
      replacer: (key, value) =>
        value instanceof Map
          ? { __type: 'Map', data: [...value.entries()] }
          : value,
      reviver: (key, value) =>
        value?.__type === 'Map' ? new Map(value.data) : value
    }
  }
});

⚠️ Sanitize before you ship

Even in development, a shared screen or an exported session can leak a real token. Treat sanitizers as mandatory for any slice that touches credentials β€” it's a one-time setup that prevents an embarrassing (or reportable) data exposure.

DevTools in Production

By default RTK already disables DevTools in production, and for most apps that's the right call β€” leaving them on ships your full state shape to anyone who opens the extension. The clean pattern is an explicit environment gate:

const isDev = process.env.NODE_ENV !== 'production';

const store = configureStore({
  reducer: rootReducer,
  // Full config in dev, completely off in prod.
  devTools: isDev
    ? { name: 'MyApp', trace: true, maxAge: 50 }
    : false
});

If you genuinely need to debug a production issue, don't flip it on for everyone β€” gate it behind a check and still sanitize aggressively:

// Enable ONLY for authorized users, and lock the risky features.
const canDebugProd = isDev || currentUser?.role === 'admin';

const store = configureStore({
  reducer: rootReducer,
  devTools: canDebugProd
    ? {
        maxAge: 25,
        actionSanitizer: hideSecrets,
        stateSanitizer: hideSecrets,
        features: {
          dispatch: false,  // don't let anyone fire arbitrary actions
          import:   false,   // don't let anyone inject a fake state
          jump:     true
        }
      }
    : false
});

βœ… The safe default

For 95% of projects, do nothing special: configureStore's built-in dev-only behavior is exactly right. Reach for the gated production config only when a bug truly can't be reproduced locally β€” and turn it back off the moment you're done.

Practice & Quiz

πŸ‹οΈ Exercise 1: A safe DevTools config

Goal: Configure a store so DevTools are enabled only in development, named "ShopApp", keep only the last 30 actions, and redact state.auth.token.

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

// TODO: build the devTools option per the requirements above.
const store = configureStore({
  reducer: rootReducer,
  devTools: /* ??? */
});
πŸ’‘ Hint

Compute isDev from process.env.NODE_ENV. Use a ternary: an options object in dev, false otherwise. Put the token redaction in stateSanitizer.

βœ… Solution
const isDev = process.env.NODE_ENV !== 'production';

const store = configureStore({
  reducer: rootReducer,
  devTools: isDev
    ? {
        name: 'ShopApp',
        maxAge: 30,
        stateSanitizer: (state) =>
          state.auth?.token
            ? { ...state, auth: { ...state.auth, token: '<<REDACTED>>' } }
            : state
      }
    : false
});

πŸ‹οΈ Exercise 2: Debug with time travel (thought exercise)

Goal: A user says the cart badge shows the wrong count after removing an item. Write the ordered steps you'd take in DevTools to find the offending action.

βœ… Solution
  1. Reproduce the bug with the Redux tab open.
  2. Scan the action list for the cart/itemRemoved dispatch.
  3. Click the action just before it β€” confirm the badge/count was correct.
  4. Click cart/itemRemoved and open the Diff tab to see exactly which fields changed.
  5. Check whether a derived count field was updated wrongly (or whether a selector, not the store, is the culprit).
  6. Fix the reducer or selector, then replay via Import to confirm.

🎯 Quick Quiz

Question 1: With Redux Toolkit's configureStore, when are DevTools enabled by default?

Question 2: What does time-travel debugging actually do to reconstruct a past state?

Question 3: How do you keep an auth token from appearing in the DevTools?

Best Practices & Pitfalls

βœ… Do

  • Let configureStore manage the dev-only default β€” don't reinvent it
  • Use the Diff tab first; it's faster than reading whole state blobs
  • Reach for time travel to pinpoint the exact action behind a bug
  • Sanitize any slice touching tokens, passwords, or payment data
  • Set a sensible maxAge so long sessions don't balloon memory
  • Export a tricky session and share the file as a reproducible bug report

❌ Don't

  • Ship DevTools enabled to production by default
  • Pass a bare devTools: true and accidentally override the prod guard
  • Let secrets flow into actions/state unsanitized
  • Treat DevTools as a substitute for real logging and error tracking
  • Leave dispatch/import features on in any production-facing config

⚠️ Non-serializable values break the timeline

DevTools serialize state to record it. Storing a Date, Map, a class instance, or a function in the store confuses the inspector and can break time travel. Keep state plain and serializable β€” RTK's default middleware even warns you when you don't β€” or teach it via the serialize option.

Summary

πŸŽ‰ Key Takeaways

  • The DevTools extension records every action, its payload, and the resulting state
  • configureStore connects and enables DevTools in dev, disables them in prod, automatically
  • The Diff tab shows precisely what changed; the Dispatcher fires actions by hand
  • Time travel replays your reducers to reconstruct any past state β€” ideal for isolating bugs
  • actionSanitizer/stateSanitizer scrub secrets before they're recorded
  • Gate any production DevTools behind an explicit check and lock down risky features

πŸ“š Additional Resources

πŸš€ What's Next?

You've now got the full advanced-Redux toolkit: normalized state, memoized selectors, and DevTools to watch it all. Time to put it to work. Next you'll build a Redux-powered e-commerce shopping cart, applying every pattern from this week in one real project.

πŸŽ‰ Debugging superpowers unlocked!

You can now inspect, diff, and rewind your app's entire history. No more guessing which action broke things.