Introduction to Redux DevTools
Redux DevTools is a powerful debugging tool that allows you to inspect every state change in your application, travel through time, and debug your Redux application effectively.
🎬 The Movie Production Analogy
Think of Redux DevTools like a movie production studio:
- State History: Recording all scenes (actions) of your movie
- Time Travel: The ability to rewind and replay any scene
- Action Replay: Playing back individual scenes to see what happened
- State Diff: Seeing changes between each take
- Action Dispatcher: The director calling "Action!" for any scene
- Export/Import: Sharing your movie script with other directors
Just as movie directors need to review and edit scenes, developers need to review and debug application state changes.
Installing Redux DevTools
1. Browser Extension Installation
// Step 1: Install the browser extension
// - Chrome: Redux DevTools Extension from Chrome Web Store
// - Firefox: Redux DevTools Add-on from Firefox Add-ons
// - Edge: Redux DevTools from Microsoft Edge Add-ons
// Step 2: Basic setup in your Redux store
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
// With middleware
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: applyMiddleware(thunk)
);
2. Advanced Setup with Compose
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from './reducers';
// Check if DevTools extension is installed
const composeEnhancers =
(typeof window !== 'undefined' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
// Create store with middleware and DevTools
const store = createStore(
rootReducer,
composeEnhancers(
applyMiddleware(thunk, logger)
)
);
// With configuration options
const composeEnhancers =
(typeof window !== 'undefined' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension options
name: 'MyApp',
trace: true,
traceLimit: 25,
})) ||
compose;
3. Redux Toolkit Setup
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
// Redux Toolkit automatically configures DevTools
const store = configureStore({
reducer: rootReducer,
// DevTools enabled by default in development
devTools: process.env.NODE_ENV !== 'production',
});
// With custom DevTools configuration
const store = configureStore({
reducer: rootReducer,
devTools: {
name: 'MyApp',
trace: true,
traceLimit: 25,
// Custom action sanitizer
actionSanitizer: (action) => {
if (action.type === 'FILE_UPLOAD') {
return { ...action, payload: '<>' };
}
return action;
},
// Custom state sanitizer
stateSanitizer: (state) => {
if (state.auth && state.auth.token) {
return {
...state,
auth: {
...state.auth,
token: '<>'
}
};
}
return state;
},
// Features configuration
features: {
pause: true, // Start/pause recording of dispatched actions
lock: true, // Lock/unlock dispatching actions
persist: true, // Persist states on page reloading
export: true, // Export history of actions in a file
import: 'custom', // Import history of actions from a file
jump: true, // Jump back and forth (time travel)
skip: true, // Skip (cancel) actions
reorder: true, // Drag and drop actions in the history list
dispatch: true, // Dispatch custom actions
test: true // Generate tests for the selected actions
}
}
});
DevTools Features
graph TD
A[Redux DevTools] --> B[Inspector]
A --> C[Log Monitor]
A --> D[Dispatcher]
A --> E[State]
A --> F[Diff]
A --> G[Chart]
A --> H[Test Generator]
B --> B1[Action List]
B --> B2[Action Details]
B --> B3[State Tree]
D --> D1[Custom Actions]
D --> D2[Action Templates]
E --> E1[Raw State]
E --> E2[State Chart]
E --> E3[State Map]
style A fill:#f96
style B fill:#9cf
style D fill:#9cf
style E fill:#9cf
1. Inspector Panel
// The Inspector shows:
// - List of dispatched actions
// - Action payload details
// - State after each action
// - Time of dispatch
// Example action in DevTools
{
type: 'todos/todoAdded',
payload: {
id: '123',
text: 'Learn Redux DevTools',
completed: false
},
meta: {
timestamp: 1678901234567
}
}
// State tree view
{
todos: {
ids: ['123'],
entities: {
'123': {
id: '123',
text: 'Learn Redux DevTools',
completed: false
}
}
},
ui: {
filter: 'all'
}
}
2. Time Travel Debugging
// Time travel allows you to:
// - Jump to any previous state
// - Replay actions in sequence
// - Skip specific actions
// - See state at any point in time
// How it works internally
const timeTravelMiddleware = store => next => action => {
// Before action
const prevState = store.getState();
// Execute action
const result = next(action);
// After action
const nextState = store.getState();
// Record state change
devToolsHistory.push({
action,
prevState,
nextState,
timestamp: Date.now()
});
return result;
};
// DevTools can then replay or skip actions
function replayActions(actions, initialState) {
return actions.reduce((state, action) => {
return rootReducer(state, action);
}, initialState);
}
3. Action Dispatcher
// Manual action dispatch from DevTools
// Use the Dispatcher tab to send custom actions
// Example dispatched action
{
type: 'ADD_TODO',
payload: {
text: 'Test from DevTools',
id: Date.now()
}
}
// Action templates for common testing
const actionTemplates = {
addUser: {
type: 'users/userAdded',
payload: {
id: '{{random.uuid}}',
name: '{{name.firstName}} {{name.lastName}}',
email: '{{internet.email}}',
createdAt: new Date().toISOString()
}
},
updateSettings: {
type: 'settings/updated',
payload: {
theme: 'dark',
notifications: true,
language: 'en'
}
},
apiError: {
type: 'api/requestFailed',
payload: {
endpoint: '/api/users',
error: 'Network Error',
status: 500
},
error: true
}
};
4. State Diff View
// DevTools shows state differences between actions
// Before action:
{
todos: [
{ id: 1, text: 'Learn Redux', completed: false }
],
filter: 'all'
}
// After action (todos/todoCompleted):
{
todos: [
{ id: 1, text: 'Learn Redux', completed: true } // Changed
],
filter: 'all'
}
// Diff view shows:
// - Changed: todos[0].completed: false → true
// - Unchanged: filter
Advanced DevTools Configuration
1. Action and State Sanitizers
// Sanitize sensitive data before sending to DevTools
const store = configureStore({
reducer: rootReducer,
devTools: {
actionSanitizer: (action) => {
// Hide sensitive data in actions
if (action.type === 'auth/loginSuccess') {
return {
...action,
payload: {
...action.payload,
token: '<>',
password: undefined
}
};
}
return action;
},
stateSanitizer: (state) => {
// Hide sensitive data in state
return {
...state,
auth: state.auth ? {
...state.auth,
token: state.auth.token ? '<>' : null,
user: state.auth.user ? {
...state.auth.user,
password: undefined,
creditCard: '<>'
} : null
} : state.auth
};
}
}
});
// Custom serialization for non-serializable data
const store = configureStore({
reducer: rootReducer,
devTools: {
serialize: {
// Handle Map objects
replacer: (key, value) => {
if (value instanceof Map) {
return {
__serializedType__: 'Map',
data: Array.from(value.entries())
};
}
if (value instanceof Set) {
return {
__serializedType__: 'Set',
data: Array.from(value)
};
}
return value;
},
// Restore Map objects
reviver: (key, value) => {
if (value && value.__serializedType__ === 'Map') {
return new Map(value.data);
}
if (value && value.__serializedType__ === 'Set') {
return new Set(value.data);
}
return value;
}
}
}
});
2. Performance Monitoring
// Monitor action execution time
const performanceMiddleware = store => next => action => {
const start = performance.now();
const result = next(action);
const end = performance.now();
const duration = end - start;
// Send performance data to DevTools
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
window.__REDUX_DEVTOOLS_EXTENSION__.send({
type: '@@PERFORMANCE',
payload: {
action: action.type,
duration: duration,
timestamp: Date.now()
}
});
}
// Log slow actions
if (duration > 16) { // Longer than one frame (60fps)
console.warn(`Slow action ${action.type}: ${duration.toFixed(2)}ms`);
}
return result;
};
// Track state size
const stateSizeMiddleware = store => next => action => {
const result = next(action);
const state = store.getState();
const stateSize = JSON.stringify(state).length;
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
window.__REDUX_DEVTOOLS_EXTENSION__.send({
type: '@@STATE_SIZE',
payload: {
size: stateSize,
sizeInKB: (stateSize / 1024).toFixed(2),
timestamp: Date.now()
}
});
}
return result;
};
3. Remote Debugging
// Remote Redux DevTools for debugging mobile apps
import { composeWithDevTools } from 'remote-redux-devtools';
const store = createStore(
rootReducer,
composeWithDevTools({
name: 'Mobile App',
realtime: true,
hostname: 'localhost',
port: 8000,
secure: false,
// Filters
actionsBlacklist: ['EFFECT_RESOLVED'],
actionsWhitelist: ['ADD_TODO', 'TOGGLE_TODO'],
// Custom instance name
instanceId: 'mobile-app-1'
})(
applyMiddleware(thunk)
)
);
// Start the remote DevTools server
// npm install -g remotedev-server
// remotedev --hostname=localhost --port=8000
DevTools in Production
1. Conditional Loading
// Only load DevTools in development
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
process.env.NODE_ENV !== 'production'
? [logger]
: []
)
});
// Feature flag for DevTools
const devToolsEnabled =
process.env.NODE_ENV !== 'production' &&
process.env.REACT_APP_ENABLE_DEVTOOLS === 'true';
const store = createStore(
rootReducer,
devToolsEnabled && window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: undefined
);
2. Production Debugging
// Enable DevTools in production for specific users
const isAdminUser = () => {
// Check if user has admin privileges
const user = JSON.parse(localStorage.getItem('user') || '{}');
return user.role === 'admin';
};
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production' || isAdminUser(),
middleware: (getDefaultMiddleware) => {
const middleware = getDefaultMiddleware();
// Add logging for admin users in production
if (process.env.NODE_ENV === 'production' && isAdminUser()) {
middleware.push(createLogger({
collapsed: true,
duration: true,
timestamp: true,
level: 'info'
}));
}
return middleware;
}
});
// Production error tracking integration
const errorTrackingMiddleware = store => next => action => {
try {
return next(action);
} catch (error) {
// Send to error tracking service
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, {
extra: {
action,
state: store.getState()
}
});
}
throw error;
}
};
DevTools Integration Examples
1. Debugging Async Actions
// Redux DevTools helps debug async action flow
const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const response = await api.getUsers();
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
// DevTools will show:
// 1. users/fetchUsers/pending
// 2. API call (in Network tab)
// 3. users/fetchUsers/fulfilled (or rejected)
// 4. State updates after each action
// Custom async action logging
const asyncActionLogger = store => next => action => {
if (action.type.endsWith('/pending')) {
console.time(`Async Action: ${action.type}`);
}
const result = next(action);
if (action.type.endsWith('/fulfilled') || action.type.endsWith('/rejected')) {
console.timeEnd(`Async Action: ${action.type.replace(/\/(fulfilled|rejected)$/, '/pending')}`);
}
return result;
};
2. Testing with DevTools
// Export state from DevTools for test cases
const exportedState = {
todos: [
{ id: 1, text: 'Test Todo', completed: false }
],
filter: 'all'
};
// Import into test
import { createStore } from 'redux';
import rootReducer from './reducers';
describe('Todo App', () => {
let store;
beforeEach(() => {
// Use exported state from DevTools
store = createStore(rootReducer, exportedState);
});
test('completes todo', () => {
store.dispatch({
type: 'todos/todoCompleted',
payload: 1
});
const state = store.getState();
expect(state.todos[0].completed).toBe(true);
});
});
// Generate tests from DevTools actions
const generateTestFromActions = (actions) => {
return `
describe('Recorded user flow', () => {
test('reproduces user actions', () => {
const store = createStore(rootReducer);
${actions.map(action => `
store.dispatch(${JSON.stringify(action)});
expect(store.getState()).toMatchSnapshot();
`).join('\n')}
});
});
`;
};
3. Performance Profiling
// Use DevTools with React Profiler
import { Profiler } from 'react';
function onRenderCallback(
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (if the tree just mounted) or "update"
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions // the Set of interactions belonging to this update
) {
// Send performance data to DevTools
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
window.__REDUX_DEVTOOLS_EXTENSION__.send({
type: '@@REACT_PERFORMANCE',
payload: {
component: id,
phase,
actualDuration,
baseDuration,
timestamp: commitTime
}
});
}
}
function App() {
return (
);
}
// Track Redux selector performance
const selectorPerformanceMiddleware = store => next => action => {
const start = performance.now();
const result = next(action);
const state = store.getState();
// Measure selector computation time
const selectorStart = performance.now();
selectExpensiveData(state);
const selectorDuration = performance.now() - selectorStart;
if (selectorDuration > 5) { // Log slow selectors
console.warn(`Slow selector execution: ${selectorDuration.toFixed(2)}ms`);
}
return result;
};
Common DevTools Workflows
1. Bug Investigation Workflow
// Step 1: Reproduce the bug
// Step 2: Open DevTools and observe action sequence
// Step 3: Use time travel to isolate the problematic action
// Step 4: Examine state diff to understand the issue
// Step 5: Fix the reducer or action creator
// Example debugging session
/*
1. User reports: "Todo items disappear after filtering"
2. DevTools shows actions:
- todos/todoAdded
- filter/filterChanged
- todos/todosCleared (unexpected!)
3. Time travel to before todos/todosCleared
4. Examine action payload and state
5. Find issue: filter change incorrectly clearing todos
6. Fix reducer logic
*/
// Bug fix example
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
// ... other reducers
},
extraReducers: (builder) => {
builder
.addCase('filter/filterChanged', (state, action) => {
// BUG: This was clearing todos
// return [];
// FIX: Don't modify todos on filter change
return state;
});
}
});
2. Performance Investigation
// Using DevTools to identify performance issues
// 1. Enable trace in DevTools
const store = configureStore({
reducer: rootReducer,
devTools: {
trace: true,
traceLimit: 25
}
});
// 2. Look for:
// - Frequent action dispatches
// - Large action payloads
// - Expensive state updates
// - Unnecessary re-renders
// 3. Use performance monitoring
const measurePerformance = (actionType) => {
const measurements = [];
return store => next => action => {
if (action.type === actionType) {
const start = performance.now();
const result = next(action);
const duration = performance.now() - start;
measurements.push(duration);
// Log statistics every 10 measurements
if (measurements.length % 10 === 0) {
const avg = measurements.reduce((a, b) => a + b) / measurements.length;
const max = Math.max(...measurements);
console.log(`${actionType} performance:`, {
average: avg.toFixed(2),
max: max.toFixed(2),
samples: measurements.length
});
}
return result;
}
return next(action);
};
};
// Apply to specific actions
const store = createStore(
rootReducer,
applyMiddleware(
measurePerformance('todos/todoAdded'),
measurePerformance('filter/filterChanged')
)
);
3. State Migration Workflow
// Using DevTools for state shape migrations
// 1. Export current state from DevTools
// 2. Transform state structure
// 3. Import transformed state
// 4. Test application functionality
// Migration script example
const migrateState = (oldState) => {
// Old state shape
// {
// todos: {
// items: [],
// filter: 'all'
// }
// }
// New state shape
// {
// todos: {
// entities: {},
// ids: []
// },
// filter: 'all'
// }
const newState = {
todos: {
entities: {},
ids: []
},
filter: oldState.todos.filter
};
// Transform todos array to normalized structure
oldState.todos.items.forEach(todo => {
newState.todos.entities[todo.id] = todo;
newState.todos.ids.push(todo.id);
});
return newState;
};
// Test migration with DevTools
// 1. Export state from production app
// 2. Run migration script
// 3. Import migrated state into development
// 4. Verify functionality
Best Practices
Do's
- Use DevTools during development for debugging
- Sanitize sensitive data before it reaches DevTools
- Use time travel to reproduce and fix bugs
- Export states and actions for testing
- Monitor performance with DevTools
- Use action filtering for large applications
Don'ts
- Don't enable DevTools in production by default
- Don't expose sensitive user data in actions/state
- Don't ignore DevTools performance warnings
- Don't use DevTools as a replacement for proper logging
- Don't forget to disable DevTools in production builds
Security Considerations
// Secure DevTools configuration
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production' ? {
// Sanitize sensitive data
actionSanitizer: (action) => {
if (action.type.includes('auth')) {
return { ...action, payload: '<>' };
}
return action;
},
stateSanitizer: (state) => ({
...state,
auth: state.auth ? {
...state.auth,
token: '<>'
} : undefined
}),
// Limit action history
maxAge: 50,
// Disable certain features
features: {
dispatch: false, // Prevent dispatching from DevTools
import: false, // Prevent importing states
}
} : false
});
Practice Exercise
Task: Set Up Advanced DevTools Configuration
Create a Redux store with advanced DevTools configuration that includes:
- Conditional loading based on environment
- Action and state sanitizers for sensitive data
- Performance monitoring middleware
- Custom action templates for testing
- Remote debugging capability
// TODO: Create advanced DevTools setup
import { configureStore } from '@reduxjs/toolkit';
import { createLogger } from 'redux-logger';
// TODO: Create sanitizer functions
const actionSanitizer = (action) => {
// Implement action sanitization
};
const stateSanitizer = (state) => {
// Implement state sanitization
};
// TODO: Create performance monitoring middleware
const performanceMiddleware = store => next => action => {
// Implement performance monitoring
};
// TODO: Create debug action templates
const debugActions = {
testAuth: {
type: 'auth/login',
payload: { /* test data */ }
},
// Add more debug actions
};
// TODO: Configure store with DevTools
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => {
// Configure middleware based on environment
},
devTools: {
// Configure DevTools options
}
});
// TODO: Add remote debugging support
const remoteDevTools = /* implement remote DevTools setup */;
// TODO: Create environment-specific configuration
const getStoreConfig = (environment) => {
// Return different configurations for dev/staging/prod
};
export default store;