State Debugging and Development Tools
1. React DevTools for State Inspection
React DevTools provides powerful state inspection, component hierarchy visualization, and performance profiling capabilities.
| DevTools Feature | Description | Use Case |
|---|---|---|
| Components Tab | Inspect component tree and props/state | View current component state values |
| State Editing | Modify state values in real-time | Test component behavior with different states |
| Hooks Inspection | View all hooks and their values | Debug useState, useReducer, useContext |
| Profiler Tab | Record and analyze render performance | Identify unnecessary re-renders |
| Component Highlighting | Visual feedback for component updates | See which components re-render |
| Search & Filter | Find components by name or type | Navigate large component trees |
Example: Inspecting Component State with DevTools
// TodoApp.jsx
import { useState, useReducer } from 'react';
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
);
default:
return state;
}
}
export function TodoApp() {
const [input, setInput] = useState('');
const [todos, dispatch] = useReducer(todoReducer, []);
const [filter, setFilter] = useState('all');
// In React DevTools Components tab, you can:
// 1. Select TodoApp component
// 2. View hooks section showing:
// - State: input (string)
// - Reducer: todos (array)
// - State: filter (string)
// 3. Edit values directly to test different scenarios
// 4. See props passed to child components
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add todo"
/>
<button onClick={() => {
dispatch({ type: 'ADD', text: input });
setInput('');
}}>Add</button>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
<ul>
{todos
.filter(t => {
if (filter === 'active') return !t.done;
if (filter === 'completed') return t.done;
return true;
})
.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'TOGGLE', id: todo.id })}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Example: Using DevTools Component Displayname for Better Debugging
// Add display names to components for better DevTools experience
import { useState, memo } from 'react';
// Functional component with display name
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ... state logic
return <div>{user?.name}</div>;
}
UserProfile.displayName = 'UserProfile';
// Memoized component with display name
const MemoizedItem = memo(function Item({ item }) {
return <div>{item.name}</div>;
});
MemoizedItem.displayName = 'MemoizedItem';
// Custom hook - shows in DevTools hooks section
function useDebugValue(value, formatFn) {
// useDebugValue shows custom label in DevTools
useDebugValue(value, v => `User: ${v?.name || 'None'}`);
return value;
}
// Higher-order component with proper naming
function withLoading(Component) {
function WithLoading(props) {
const [loading, setLoading] = useState(true);
// ... loading logic
return loading ? <div>Loading...</div> : <Component {...props} />;
}
WithLoading.displayName = `WithLoading(${Component.displayName || Component.name})`;
return WithLoading;
}
// Usage in DevTools:
// - Components show meaningful names instead of "Anonymous"
// - Custom hooks display formatted debug values
// - HOCs show wrapped component name for easier debugging
Note: Enable "Highlight updates when components render" in DevTools settings to visually see
which components re-render on state changes. This helps identify performance issues.
2. Redux DevTools Extension Integration
Redux DevTools Extension provides time-travel debugging, action history, and state diff visualization for Redux applications.
| Feature | Description | Use Case |
|---|---|---|
| Action History | View all dispatched actions chronologically | Track state changes over time |
| State Diff | See state changes for each action | Understand action impact |
| Time Travel | Jump to any previous state | Debug specific state scenarios |
| Action Replay | Replay actions from history | Reproduce bugs |
| State Export/Import | Save and load application state | Share bug reproductions |
| Action Filtering | Show/hide specific action types | Focus on relevant actions |
Example: Redux Toolkit with DevTools Integration
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import userReducer from './userSlice';
// Redux Toolkit automatically enables Redux DevTools in development
export const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
// DevTools configuration (optional)
devTools: process.env.NODE_ENV !== 'production' ? {
name: 'My App',
trace: true, // Enable action stack traces
traceLimit: 25,
features: {
pause: true, // Pause recording
lock: true, // Lock/unlock state changes
persist: true, // Persist state in localStorage
export: true, // Export state
import: 'custom', // Import state
jump: true, // Jump to action
skip: true, // Skip actions
reorder: true, // Reorder actions
dispatch: true, // Dispatch custom actions
test: true // Generate tests
}
} : false
});
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, history: [] },
reducers: {
increment: (state) => {
state.value += 1;
// DevTools will show:
// Action: counter/increment
// Diff: +value: 1
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
// DevTools shows payload value in action details
},
// Add action metadata for better DevTools display
reset: {
reducer: (state) => {
state.value = 0;
state.history = [];
},
prepare: () => ({
payload: undefined,
meta: { timestamp: Date.now(), reason: 'user_reset' }
})
}
}
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
Example: Custom Redux DevTools Integration for Non-Redux State
// Custom hook with Redux DevTools integration
import { useReducer, useEffect, useRef } from 'react';
function useReducerWithDevTools(reducer, initialState, name = 'State') {
const [state, dispatch] = useReducer(reducer, initialState);
const devTools = useRef(null);
useEffect(() => {
// Connect to Redux DevTools Extension
if (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
devTools.current = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
name,
features: { jump: true, skip: true }
});
// Send initial state
devTools.current.init(initialState);
}
return () => {
if (devTools.current) {
devTools.current.unsubscribe();
}
};
}, [name, initialState]);
// Wrapper dispatch that notifies DevTools
const dispatchWithDevTools = (action) => {
dispatch(action);
if (devTools.current) {
// Send action to DevTools
devTools.current.send(action, reducer(state, action));
}
};
// Subscribe to DevTools messages (time travel)
useEffect(() => {
if (!devTools.current) return;
const unsubscribe = devTools.current.subscribe((message) => {
if (message.type === 'DISPATCH' && message.state) {
// Handle time-travel: update state from DevTools
const newState = JSON.parse(message.state);
// Note: This requires modifying the reducer to accept a SET_STATE action
dispatch({ type: '@@DEVTOOLS/SET_STATE', payload: newState });
}
});
return unsubscribe;
}, []);
return [state, dispatchWithDevTools];
}
// Usage
function TodoApp() {
const [state, dispatch] = useReducerWithDevTools(
todoReducer,
{ todos: [], filter: 'all' },
'TodoApp'
);
// Now your useReducer state appears in Redux DevTools!
// You can time-travel, see action history, export state, etc.
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'Learn DevTools' })}>
Add Todo
</button>
{/* ... */}
</div>
);
}
Note: Redux DevTools can integrate with any state management solution, not just Redux. Use
the
__REDUX_DEVTOOLS_EXTENSION__ API to connect custom state managers.
3. State Logging and Debug Patterns
Implement structured logging patterns to track state changes and debug issues in development and production.
| Logging Pattern | Description | Use Case |
|---|---|---|
| Console logging | Log state changes to browser console | Development debugging |
| Middleware logging | Redux middleware for action logging | Track all state mutations |
| State snapshots | Capture state at specific points | Compare state before/after |
| Conditional logging | Log only in specific conditions | Filter noise, focus on issues |
| Performance logging | Log render times and counts | Identify performance bottlenecks |
Example: Custom useDebugState Hook with Logging
// useDebugState.js
import { useState, useEffect, useRef } from 'react';
export function useDebugState(initialValue, name = 'State') {
const [state, setState] = useState(initialValue);
const previousState = useRef(initialValue);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
const setStateWithDebug = (newValue) => {
const valueToSet = typeof newValue === 'function'
? newValue(state)
: newValue;
console.group(`šµ ${name} State Update`);
console.log('Previous:', previousState.current);
console.log('New:', valueToSet);
console.log('Render Count:', renderCount.current);
console.log('Timestamp:', new Date().toISOString());
console.trace('Update triggered from:');
console.groupEnd();
previousState.current = state;
setState(valueToSet);
};
// Log on every render in development
if (process.env.NODE_ENV === 'development') {
console.log(`š ${name} rendered:`, state, `(${renderCount.current} times)`);
}
return [state, setStateWithDebug];
}
// Usage
function Counter() {
const [count, setCount] = useDebugState(0, 'Counter');
const [step, setStep] = useDebugState(1, 'Step');
return (
<div>
<p>Count: {count}</p>
<p>Step: {step}</p>
<button onClick={() => setCount(c => c + step)}>
Increment by {step}
</button>
<button onClick={() => setStep(s => s + 1)}>
Increase Step
</button>
</div>
);
// Console output when clicking Increment:
// š Counter rendered: 0 (1 times)
// š Step rendered: 1 (1 times)
// šµ Counter State Update
// Previous: 0
// New: 1
// Render Count: 1
// Timestamp: 2025-12-20T10:30:45.123Z
// Update triggered from: [stack trace]
}
Example: Redux Logger Middleware
// Custom logger middleware
const loggerMiddleware = (store) => (next) => (action) => {
const prevState = store.getState();
const startTime = performance.now();
console.group(`ā” Action: ${action.type}`);
console.log('Payload:', action.payload);
console.log('Previous State:', prevState);
// Call the next middleware or reducer
const result = next(action);
const nextState = store.getState();
const endTime = performance.now();
console.log('Next State:', nextState);
console.log('State Diff:', getStateDiff(prevState, nextState));
console.log(`ā±ļø Time: ${(endTime - startTime).toFixed(2)}ms`);
console.groupEnd();
return result;
};
// Helper to show state differences
function getStateDiff(prev, next) {
const diff = {};
Object.keys(next).forEach(key => {
if (prev[key] !== next[key]) {
diff[key] = { from: prev[key], to: next[key] };
}
});
return diff;
}
// Redux Toolkit setup with logger
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger'; // Or use custom logger above
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
process.env.NODE_ENV === 'development' ? logger : []
)
});
// Conditional logging based on action type
const selectiveLogger = (store) => (next) => (action) => {
// Only log specific actions
const actionsToLog = ['user/login', 'cart/addItem', 'order/submit'];
if (actionsToLog.some(type => action.type.includes(type))) {
console.log('šÆ Important Action:', action);
console.log('Current State:', store.getState());
}
return next(action);
};
Example: Production-Safe State Logging
// logger.js - Production-safe logging utility
class StateLogger {
constructor(options = {}) {
this.enabled = options.enabled ?? process.env.NODE_ENV === 'development';
this.maxLogs = options.maxLogs ?? 100;
this.logs = [];
this.excludeActions = options.excludeActions ?? [];
}
log(type, data) {
if (!this.enabled) return;
const logEntry = {
type,
data,
timestamp: Date.now(),
url: window.location.href
};
this.logs.push(logEntry);
// Keep only recent logs
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// Console output in development only
if (process.env.NODE_ENV === 'development') {
console.log(`[${type}]`, data);
}
}
logStateChange(actionType, prevState, nextState) {
if (this.excludeActions.includes(actionType)) return;
this.log('STATE_CHANGE', {
action: actionType,
before: this.sanitizeState(prevState),
after: this.sanitizeState(nextState)
});
}
// Remove sensitive data before logging
sanitizeState(state) {
const sanitized = { ...state };
const sensitiveKeys = ['password', 'token', 'creditCard', 'ssn'];
Object.keys(sanitized).forEach(key => {
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
// Export logs for bug reports
exportLogs() {
return JSON.stringify(this.logs, null, 2);
}
// Download logs as file
downloadLogs() {
const blob = new Blob([this.exportLogs()], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `state-logs-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
// Send logs to error tracking service
async sendToErrorTracking(error) {
if (process.env.NODE_ENV === 'production') {
// Send to Sentry, LogRocket, etc.
await fetch('/api/error-logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.message,
stack: error.stack,
stateLogs: this.logs
})
});
}
}
}
export const stateLogger = new StateLogger({
enabled: true,
excludeActions: ['@@INIT', 'persist/REHYDRATE']
});
Warning: Never log sensitive data (passwords, tokens, PII) in production. Always sanitize
state before logging and disable verbose logging in production builds.
4. Time-travel Debugging for State Changes
Implement time-travel debugging to replay state changes and jump to any previous application state.
| Technique | Description | Use Case |
|---|---|---|
| State history stack | Store all state snapshots | Navigate through past states |
| Action replay | Re-execute actions from history | Reproduce specific scenarios |
| State restoration | Jump to any historical state | Debug specific state configurations |
| Undo/Redo | Navigate back and forward in time | User-facing time travel features |
Example: Custom Time-Travel Hook
// useTimeTravel.js
import { useReducer, useCallback, useRef } from 'react';
function timeTravelReducer(history, action) {
const { past, present, future } = history;
switch (action.type) {
case 'UNDO':
if (past.length === 0) return history;
return {
past: past.slice(0, past.length - 1),
present: past[past.length - 1],
future: [present, ...future]
};
case 'REDO':
if (future.length === 0) return history;
return {
past: [...past, present],
present: future[0],
future: future.slice(1)
};
case 'SET':
if (action.payload === present) return history;
return {
past: [...past, present],
present: action.payload,
future: []
};
case 'RESET':
return {
past: [],
present: action.payload,
future: []
};
case 'JUMP_TO':
// Jump to specific index in history
const index = action.payload;
const allStates = [...past, present, ...future];
if (index < 0 || index >= allStates.length) return history;
return {
past: allStates.slice(0, index),
present: allStates[index],
future: allStates.slice(index + 1)
};
default:
return history;
}
}
export function useTimeTravel(initialState, maxHistory = 50) {
const [history, dispatch] = useReducer(timeTravelReducer, {
past: [],
present: initialState,
future: []
});
const { past, present, future } = history;
const setState = useCallback((newState) => {
dispatch({ type: 'SET', payload: newState });
}, []);
const undo = useCallback(() => {
dispatch({ type: 'UNDO' });
}, []);
const redo = useCallback(() => {
dispatch({ type: 'REDO' });
}, []);
const reset = useCallback((state = initialState) => {
dispatch({ type: 'RESET', payload: state });
}, [initialState]);
const jumpTo = useCallback((index) => {
dispatch({ type: 'JUMP_TO', payload: index });
}, []);
const canUndo = past.length > 0;
const canRedo = future.length > 0;
// Get all history for visualization
const allHistory = [...past, present, ...future];
const currentIndex = past.length;
return {
state: present,
setState,
undo,
redo,
reset,
jumpTo,
canUndo,
canRedo,
history: allHistory,
currentIndex
};
}
// Usage
function DrawingApp() {
const {
state: canvas,
setState: setCanvas,
undo,
redo,
canUndo,
canRedo,
history,
currentIndex,
jumpTo
} = useTimeTravel({ shapes: [] });
const addShape = (shape) => {
setCanvas({ shapes: [...canvas.shapes, shape] });
};
return (
<div>
<div>
<button onClick={undo} disabled={!canUndo}>
ā¬
ļø Undo
</button>
<button onClick={redo} disabled={!canRedo}>
ā”ļø Redo
</button>
</div>
{/* Timeline visualization */}
<div style={{ display: 'flex', gap: '4px' }}>
{history.map((state, index) => (
<button
key={index}
onClick={() => jumpTo(index)}
style={{
background: index === currentIndex ? 'blue' : 'gray',
width: '20px',
height: '20px'
}}
title={`State ${index}: ${state.shapes.length} shapes`}
/>
))}
</div>
<Canvas shapes={canvas.shapes} onAddShape={addShape} />
</div>
);
}
Example: Redux DevTools Time-Travel Integration
// Using Redux DevTools for automatic time-travel
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
export const store = configureStore({
reducer: rootReducer,
devTools: {
name: 'My App',
features: {
jump: true, // Enable jumping to any action
skip: true, // Skip (ignore) actions
reorder: true, // Reorder actions
pause: true // Pause action recording
}
}
});
// In your component, Redux DevTools provides:
// 1. Timeline slider to scrub through states
// 2. Jump to any previous action
// 3. Skip/ignore specific actions
// 4. Replay action sequences
// You can also access DevTools programmatically:
function DebugPanel() {
const exportState = () => {
const state = store.getState();
const blob = new Blob([JSON.stringify(state, null, 2)], {
type: 'application/json'
});
// Download state snapshot
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `app-state-${Date.now()}.json`;
a.click();
};
const importState = (stateJson) => {
const state = JSON.parse(stateJson);
// Restore state through DevTools
window.__REDUX_DEVTOOLS_EXTENSION__?.send(
{ type: '@@IMPORT_STATE' },
state
);
};
return (
<div>
<button onClick={exportState}>Export Current State</button>
<input
type="file"
accept=".json"
onChange={(e) => {
const file = e.target.files[0];
file.text().then(importState);
}}
/>
</div>
);
}
Note: Time-travel debugging works best with immutable state updates. Ensure all state changes
create new objects rather than mutating existing ones for accurate history tracking.
5. Performance Profiling for State Updates
Profile and measure state update performance to identify bottlenecks and optimize re-renders.
| Profiling Tool | Description | Use Case |
|---|---|---|
| React Profiler | Record component render times | Identify slow components |
| Performance API | Measure precise timing with marks | Custom performance tracking |
| Why Did You Render | Library to detect unnecessary renders | Find optimization opportunities |
| Chrome DevTools | Timeline recording and flame graphs | Analyze render performance |
Example: React Profiler Component for State Updates
// ProfiledComponent.jsx
import { Profiler } from 'react';
function onRenderCallback(
id, // The "id" prop of the Profiler tree
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When React began rendering
commitTime, // When React committed the update
interactions // Set of interactions for this update
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
renderTime: commitTime - startTime,
interactions
});
// Send to analytics in production
if (actualDuration > 16) { // Slower than 60fps
console.warn(`ā ļø Slow render in ${id}: ${actualDuration}ms`);
// Send to monitoring service
sendToAnalytics({
type: 'slow_render',
component: id,
duration: actualDuration,
phase
});
}
}
export function ProfiledApp() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>
);
}
// Profile specific components with state
function TodoList() {
const [todos, setTodos] = useState([]);
return (
<Profiler id="TodoList" onRender={onRenderCallback}>
<div>
{todos.map(todo => (
<Profiler key={todo.id} id={`Todo-${todo.id}`} onRender={onRenderCallback}>
<TodoItem todo={todo} />
</Profiler>
))}
</div>
</Profiler>
);
}
Example: Custom Performance Monitoring Hook
// usePerformanceMonitor.js
import { useEffect, useRef } from 'react';
export function usePerformanceMonitor(componentName, dependencies = []) {
const renderCount = useRef(0);
const renderTimes = useRef([]);
const lastRenderTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const now = performance.now();
const renderTime = now - lastRenderTime.current;
renderTimes.current.push(renderTime);
// Keep only last 100 render times
if (renderTimes.current.length > 100) {
renderTimes.current.shift();
}
lastRenderTime.current = now;
// Calculate statistics
const avgRenderTime =
renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length;
const maxRenderTime = Math.max(...renderTimes.current);
console.log(`š ${componentName} Performance`, {
renderCount: renderCount.current,
lastRenderTime: renderTime.toFixed(2) + 'ms',
avgRenderTime: avgRenderTime.toFixed(2) + 'ms',
maxRenderTime: maxRenderTime.toFixed(2) + 'ms',
dependencies
});
});
return {
renderCount: renderCount.current,
avgRenderTime:
renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length
};
}
// useWhyDidYouUpdate - Track which props/state caused re-render
export function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach((key) => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changedProps).length > 0) {
console.log(`š ${name} re-rendered due to:`, changedProps);
}
}
previousProps.current = props;
});
}
// Usage
function ExpensiveComponent({ data, filter, onUpdate }) {
usePerformanceMonitor('ExpensiveComponent', [data, filter]);
useWhyDidYouUpdate('ExpensiveComponent', { data, filter, onUpdate });
const filteredData = useMemo(
() => data.filter(filter),
[data, filter]
);
return (
<div>
{filteredData.map(item => (
<Item key={item.id} item={item} />
))}
</div>
);
}
Example: Redux Performance Monitoring Middleware
// performanceMiddleware.js
const performanceMiddleware = (store) => (next) => (action) => {
const startTime = performance.now();
const startMark = `action-${action.type}-start`;
const endMark = `action-${action.type}-end`;
// Mark start
performance.mark(startMark);
// Execute action
const result = next(action);
// Mark end
performance.mark(endMark);
// Measure duration
const measureName = `action-${action.type}`;
performance.measure(measureName, startMark, endMark);
const endTime = performance.now();
const duration = endTime - startTime;
// Get performance entry
const entries = performance.getEntriesByName(measureName);
const entry = entries[entries.length - 1];
// Log slow actions
if (duration > 16) { // Slower than one frame (60fps)
console.warn('ā ļø Slow action detected:', {
action: action.type,
duration: duration.toFixed(2) + 'ms',
payload: action.payload
});
}
// Aggregate statistics
if (!window.__ACTION_STATS__) {
window.__ACTION_STATS__ = {};
}
if (!window.__ACTION_STATS__[action.type]) {
window.__ACTION_STATS__[action.type] = {
count: 0,
totalTime: 0,
minTime: Infinity,
maxTime: 0
};
}
const stats = window.__ACTION_STATS__[action.type];
stats.count += 1;
stats.totalTime += duration;
stats.minTime = Math.min(stats.minTime, duration);
stats.maxTime = Math.max(stats.maxTime, duration);
stats.avgTime = stats.totalTime / stats.count;
// Clean up performance marks
performance.clearMarks(startMark);
performance.clearMarks(endMark);
performance.clearMeasures(measureName);
return result;
};
// View statistics in console
window.viewActionStats = () => {
console.table(
Object.entries(window.__ACTION_STATS__).map(([type, stats]) => ({
Action: type,
Count: stats.count,
'Avg (ms)': stats.avgTime.toFixed(2),
'Min (ms)': stats.minTime.toFixed(2),
'Max (ms)': stats.maxTime.toFixed(2),
'Total (ms)': stats.totalTime.toFixed(2)
}))
);
};
// Usage: Run viewActionStats() in console to see performance breakdown
Note: Use React DevTools Profiler tab to record and analyze render performance visually. Look
for components with long render times or frequent unnecessary re-renders.
6. Custom Debug Hooks for State Monitoring
Create reusable debug hooks to monitor state changes, track renders, and identify performance issues.
| Debug Hook | Purpose | Use Case |
|---|---|---|
| useDebugValue | Label custom hooks in DevTools | Display formatted debug info |
| useTraceUpdate | Log which props/state changed | Find cause of re-renders |
| useRenderCount | Count component renders | Detect excessive re-renders |
| useStateLogger | Log all state changes | Track state evolution |
Example: Comprehensive Debug Hook Collection
// debugHooks.js
import { useRef, useEffect, useDebugValue } from 'react';
// 1. useTraceUpdate - Find what caused re-render
export function useTraceUpdate(props, componentName = 'Component') {
const prev = useRef(props);
useEffect(() => {
const changedProps = Object.entries(props).reduce((acc, [key, value]) => {
if (prev.current[key] !== value) {
acc[key] = { from: prev.current[key], to: value };
}
return acc;
}, {});
if (Object.keys(changedProps).length > 0) {
console.log(`[${componentName}] Changed props:`, changedProps);
}
prev.current = props;
});
}
// 2. useRenderCount - Count renders
export function useRenderCount(componentName = 'Component') {
const renders = useRef(0);
renders.current += 1;
useEffect(() => {
console.log(`[${componentName}] Render #${renders.current}`);
});
useDebugValue(`Rendered ${renders.current} times`);
return renders.current;
}
// 3. useStateWithHistory - State with history tracking
export function useStateWithHistory(initialValue, capacity = 10) {
const [value, setValue] = useState(initialValue);
const history = useRef([initialValue]);
const pointer = useRef(0);
const set = useCallback((newValue) => {
const resolvedValue = typeof newValue === 'function'
? newValue(value)
: newValue;
if (history.current[pointer.current] !== resolvedValue) {
// Remove future history if we're not at the end
if (pointer.current < history.current.length - 1) {
history.current.splice(pointer.current + 1);
}
history.current.push(resolvedValue);
// Limit history size
if (history.current.length > capacity) {
history.current.shift();
} else {
pointer.current += 1;
}
setValue(resolvedValue);
}
}, [value, capacity]);
const back = useCallback(() => {
if (pointer.current > 0) {
pointer.current -= 1;
setValue(history.current[pointer.current]);
}
}, []);
const forward = useCallback(() => {
if (pointer.current < history.current.length - 1) {
pointer.current += 1;
setValue(history.current[pointer.current]);
}
}, []);
const go = useCallback((index) => {
if (index >= 0 && index < history.current.length) {
pointer.current = index;
setValue(history.current[index]);
}
}, []);
useDebugValue(`History: ${pointer.current + 1}/${history.current.length}`);
return {
value,
setValue: set,
history: history.current,
pointer: pointer.current,
back,
forward,
go,
canGoBack: pointer.current > 0,
canGoForward: pointer.current < history.current.length - 1
};
}
// 4. useDebugState - Enhanced useState with logging
export function useDebugState(initialValue, name = 'State') {
const [value, setValue] = useState(initialValue);
const renders = useRef(0);
renders.current += 1;
const debugSetValue = useCallback((newValue) => {
const resolvedValue = typeof newValue === 'function'
? newValue(value)
: newValue;
console.log(`[${name}] State change:`, {
from: value,
to: resolvedValue,
render: renders.current
});
setValue(resolvedValue);
}, [value, name]);
useDebugValue(`${name}: ${JSON.stringify(value)}`);
return [value, debugSetValue];
}
// 5. useEffectDebugger - Debug useEffect dependencies
export function useEffectDebugger(effectFn, dependencies, name = 'Effect') {
const previousDeps = useRef(dependencies);
useEffect(() => {
const changedDeps = dependencies.reduce((acc, dep, index) => {
if (dep !== previousDeps.current[index]) {
acc.push({
index,
before: previousDeps.current[index],
after: dep
});
}
return acc;
}, []);
if (changedDeps.length > 0) {
console.log(`[${name}] Effect triggered by:`, changedDeps);
}
previousDeps.current = dependencies;
return effectFn();
}, dependencies);
}
// 6. useComponentDidMount - Track mount/unmount
export function useComponentDidMount(componentName = 'Component') {
useEffect(() => {
console.log(`ā
[${componentName}] Mounted`);
return () => {
console.log(`ā [${componentName}] Unmounted`);
};
}, [componentName]);
}
Example: Using Debug Hooks in Components
// UserProfile.jsx
import {
useTraceUpdate,
useRenderCount,
useDebugState,
useEffectDebugger,
useComponentDidMount
} from './debugHooks';
function UserProfile({ userId, theme, onUpdate }) {
// Track what causes re-renders
useTraceUpdate({ userId, theme, onUpdate }, 'UserProfile');
// Count renders
const renderCount = useRenderCount('UserProfile');
// Track mount/unmount
useComponentDidMount('UserProfile');
// Debug state changes
const [user, setUser] = useDebugState(null, 'User');
const [loading, setLoading] = useDebugState(true, 'Loading');
// Debug effect dependencies
useEffectDebugger(
() => {
const fetchUser = async () => {
setLoading(true);
const data = await fetch(`/api/users/${userId}`).then(r => r.json());
setUser(data);
setLoading(false);
};
fetchUser();
},
[userId],
'FetchUser'
);
if (loading) return <div>Loading...</div>;
return (
<div className={theme}>
<h1>{user?.name}</h1>
<p>Render count: {renderCount}</p>
</div>
);
}
// Console output when userId changes:
// [UserProfile] Changed props: { userId: { from: 1, to: 2 } }
// [UserProfile] Render #2
// [FetchUser] Effect triggered by: [{ index: 0, before: 1, after: 2 }]
// [Loading] State change: { from: false, to: true, render: 2 }
// [User] State change: { from: {...}, to: {...}, render: 3 }
// [Loading] State change: { from: true, to: false, render: 3 }
Example: Production-Safe Debug Wrapper
// Production-safe debug hooks that only run in development
const isDevelopment = process.env.NODE_ENV === 'development';
export const useDebugHooks = {
useTraceUpdate: isDevelopment
? useTraceUpdate
: () => {},
useRenderCount: isDevelopment
? useRenderCount
: () => 0,
useDebugState: isDevelopment
? useDebugState
: useState,
useEffectDebugger: isDevelopment
? useEffectDebugger
: useEffect,
useComponentDidMount: isDevelopment
? useComponentDidMount
: () => {}
};
// Usage - automatically disabled in production
import { useDebugHooks } from './debugHooks';
function MyComponent(props) {
useDebugHooks.useTraceUpdate(props, 'MyComponent');
const renderCount = useDebugHooks.useRenderCount('MyComponent');
const [state, setState] = useDebugHooks.useDebugState(0, 'Counter');
// These hooks do nothing in production builds
return <div>Count: {state}</div>;
}
Note: Use
useDebugValue in custom hooks to display formatted debug information
in
React DevTools. This helps identify hook values without console logging.
Section 18 Key Takeaways
- React DevTools - Inspect component state, edit values in real-time, use Profiler to identify performance issues
- Redux DevTools - Time-travel debugging, action history, state diff visualization, export/import state
- State logging - Implement structured logging, sanitize sensitive data, disable verbose logging in production
- Time-travel - Store state history, implement undo/redo, jump to any previous state for debugging
- Performance profiling - Use React Profiler, track render times, identify slow actions with middleware
- Debug hooks - Create reusable hooks for tracing updates, counting renders, logging state changes