π οΈ 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
configureStoreenables by default and how to customizedevTools - 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/stateSanitizerbefore 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.
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 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
- Reproduce the bug with the Redux tab open.
- Scan the action list for the
cart/itemRemoveddispatch. - Click the action just before it β confirm the badge/count was correct.
- Click
cart/itemRemovedand open the Diff tab to see exactly which fields changed. - Check whether a derived count field was updated wrongly (or whether a selector, not the store, is the culprit).
- 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
configureStoremanage 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
maxAgeso 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: trueand 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/importfeatures 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
configureStoreconnects 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/stateSanitizerscrub secrets before they're recorded- Gate any production DevTools behind an explicit check and lock down risky features
π Additional Resources
- Redux β Integrating the DevTools Extension
- Redux Toolkit β configureStore devTools option
- Redux DevTools β official repository
- Redux DevTools Extension β docs & options
π 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.