React DevTools and Debugging
1. React DevTools Browser Extension (Chrome, Firefox)
| Feature | Purpose | Access Method | Key Capabilities |
|---|---|---|---|
| Components Tab | Inspect component tree and props/state | Browser DevTools → React tab | View hierarchy, search components, inspect props/state/hooks |
| Profiler Tab | Measure rendering performance | Browser DevTools → Profiler tab | Record renders, flame graphs, ranked charts, commit details |
| Settings | Configure DevTools behavior | Gear icon in DevTools | Highlight updates, trace updates, component filters, theme |
| Highlight Updates | Visually show component re-renders | Settings → Highlight updates | Color-coded borders around re-rendering components |
| Strict Mode Badge | Indicates Strict Mode enabled | Component tree icons | Shows which components wrapped in StrictMode |
| Component Filters | Hide unwanted components from tree | Settings → Component filters | Filter by name, location, or HOC patterns |
Example: Installing React DevTools
// Chrome Web Store
https://chrome.google.com/webstore (search "React Developer Tools")
// Firefox Add-ons
https://addons.mozilla.org (search "React Developer Tools")
// Edge Add-ons
https://microsoftedge.microsoft.com/addons (search "React Developer Tools")
// Standalone (Electron app)
npm install -g react-devtools
react-devtools
// Connect standalone to app
<script src="http://localhost:8097"></script>
Example: Accessing DevTools features
// Enable highlight updates to see re-renders
// 1. Open React DevTools
// 2. Click Settings (gear icon)
// 3. Check "Highlight updates when components render"
// 4. Watch colored borders appear on updates
// Search for components
// 1. Click in Components tab
// 2. Press Cmd+F (Mac) or Ctrl+F (Windows)
// 3. Type component name
// 4. Navigate with Enter/Shift+Enter
// View component source
// 1. Select component in tree
// 2. Right panel shows props, state, hooks
// 3. Click "<>" icon to view source code
// 4. Opens in Sources tab at component definition
DevTools Shortcuts: Cmd/Ctrl + F (search), Cmd/Ctrl + P (search by component name), Click
component in page to select in tree (requires "Inspect" feature enabled), Click eye icon to inspect DOM node,
Use filter input to narrow tree view.
2. Component Tree Inspector and Props Viewer
| Inspector Feature | Information Shown | Interaction | Use Case |
|---|---|---|---|
| Props Panel | All props passed to component | View values, edit primitives | Debug incorrect prop values, test different prop combinations |
| State Panel | Component state values | View and edit state | Test different states, reproduce bugs, validate state logic |
| Hooks Panel | All hooks in component with values | View hook state, effects | Debug custom hooks, verify hook dependencies |
| Rendered By | Parent component that rendered this component | Click to navigate to parent | Understand component hierarchy and data flow |
| Owners | Component ownership chain | Shows entire owner hierarchy | Trace where props originated, debug context issues |
| Source Location | File and line number of component | Click to open in Sources tab | Quick navigation to component code |
Example: Editing props and state in DevTools
// Original component
function Counter() {
const [count, setCount] = useState(0);
return <div>Count: {count}</div>;
}
// In React DevTools:
// 1. Select Counter component
// 2. In right panel, see "State" section
// 3. Click on "count: 0" value
// 4. Edit to different value (e.g., 100)
// 5. Press Enter - component re-renders with new value
// Editing props (parent component needed)
function Parent() {
return <Child name="Alice" age={25} />;
}
// In DevTools:
// 1. Select Child component
// 2. See "Props" section: name: "Alice", age: 25
// 3. Edit primitive values directly
// 4. Note: Complex objects/functions can't be edited
Example: Debugging hooks with DevTools
function UserProfile() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser().then(setUser).catch(setError).finally(() => setLoading(false));
}, []);
const memoizedUser = useMemo(() => formatUser(user), [user]);
const handleClick = useCallback(() => console.log(user), [user]);
// ...
}
// In DevTools, Hooks section shows:
// State (0): true // loading
// State (1): null // user
// State (2): null // error
// Effect // useEffect
// Memo // useMemo
// Callback // useCallback
// You can:
// - Edit state values directly
// - See dependencies for useMemo/useCallback
// - Track which effects ran
// - Identify custom hooks by name
Example: Using "Rendered by" to trace component tree
// Component hierarchy
<App>
<Dashboard>
<UserPanel>
<UserAvatar src={user.avatar} />
</UserPanel>
</Dashboard>
</App>
// Select UserAvatar in DevTools
// "Rendered by" section shows:
// UserPanel → Dashboard → App
// Click any parent to navigate
// See full props chain:
// - App passes user to Dashboard
// - Dashboard passes user to UserPanel
// - UserPanel passes user.avatar to UserAvatar
3. React Profiler and Performance Analysis
| Profiler Feature | Visualization | Metrics | Analysis Use |
|---|---|---|---|
| Flamegraph Chart | Hierarchical view of render times | Render duration per component | Identify slow components, bottlenecks in tree |
| Ranked Chart | Sorted list of components by render time | Duration, sorted slowest first | Quickly find most expensive renders |
| Component Chart | Timeline of individual component renders | Each commit, duration, props/state changes | Track re-render frequency, identify unnecessary renders |
| Commit Details | Specific render commit information | What triggered render, duration, timestamp | Understand why component re-rendered |
| Interactions | User interactions that triggered renders | Which user actions caused updates | Measure interaction responsiveness |
| Why Did This Render | Explanation of render cause | Props changed, state changed, parent rendered | Debug unnecessary re-renders |
Example: Using Profiler to identify performance issues
// Steps to profile your app:
// 1. Open React DevTools → Profiler tab
// 2. Click "Start Profiling" (blue record button)
// 3. Interact with your app (click buttons, type, navigate)
// 4. Click "Stop Profiling" (red square button)
// 5. Analyze the results
// Reading Flamegraph:
// - Wider bars = longer render time
// - Height = component depth in tree
// - Colors: Yellow (fast), Orange (medium), Red (slow)
// - Click bar to see component details
// Reading Ranked Chart:
// - Components sorted by render duration
// - Percentage shows relative time
// - Focus on top components first
// Example findings:
// Component: BigList
// Rendered: 15 times
// Duration: 1,245ms total
// Cause: Parent state change (unnecessary)
// Fix: Wrap in React.memo()
Example: Profiler API for custom measurements
import { Profiler } from 'react';
function onRenderCallback(
id, // "panel" - profiler ID
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When render started
commitTime, // When React committed update
interactions // Set of interactions
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
// Send to analytics
analytics.track('component-render', {
component: id,
phase,
duration: actualDuration,
wasSlower: actualDuration > baseDuration
});
}
function App() {
return (
<Profiler id="dashboard" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}
// Nested profilers for granular tracking
function Dashboard() {
return (
<div>
<Profiler id="sidebar" onRender={onRenderCallback}>
<Sidebar />
</Profiler>
<Profiler id="content" onRender={onRenderCallback}>
<Content />
</Profiler>
</div>
);
}
Example: Analyzing "Why Did This Render"
// Enable in Settings → General → "Record why each component rendered"
// Example analysis in DevTools:
// Component: TodoItem
// Rendered because: Props changed
// Changed props:
// - isCompleted: false → true
// - onClick: [function] → [function] (different reference!)
// Problem identified: onClick recreated on every render
// Before (bad):
function TodoList() {
const [todos, setTodos] = useState([]);
return todos.map(todo => (
<TodoItem
todo={todo}
onClick={() => handleToggle(todo.id)} // New function every render!
/>
));
}
// After (good):
function TodoList() {
const [todos, setTodos] = useState([]);
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
}, []);
return todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle} // Stable reference
/>
));
}
const TodoItem = React.memo(({ todo, onToggle }) => (
<li onClick={() => onToggle(todo.id)}>{todo.text}</li>
));
4. State and Context Debugging Tools
| Technique | Tool/Method | What to Check | Common Issues Found |
|---|---|---|---|
| useState Inspection | DevTools Hooks panel | Current state values, update functions | Stale closures, incorrect initial state, batching issues |
| useReducer Debugging | DevTools + Redux DevTools | State shape, action history | Incorrect action handling, state mutations, missing cases |
| Context Values | DevTools Context panel | Current context value, provider location | Missing provider, wrong value, too many re-renders |
| State History | Custom logging, Redux DevTools | State changes over time | Unexpected mutations, missing updates |
| State Synchronization | Multiple component inspection | Shared state consistency | Race conditions, stale data, caching issues |
| Derived State | useMemo inspection | Memoized values, dependencies | Missing dependencies, over-computation |
Example: Debugging context issues
// Context not updating problem
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// Problem: New object every render!
const value = { user, setUser };
return (
<UserContext.Provider value={value}>
<Dashboard />
</UserContext.Provider>
);
}
// In DevTools:
// 1. Select any consumer component
// 2. Check "Context" in right panel
// 3. See value changing every render (object identity)
// 4. All consumers re-render unnecessarily
// Fix:
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
<Dashboard />
</UserContext.Provider>
);
}
Example: Using Redux DevTools with useReducer
import { useReducer } from 'react';
// Install Redux DevTools Extension for full state tracking
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, action.payload];
case 'REMOVE':
return state.filter(todo => todo.id !== action.payload);
case 'TOGGLE':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
}
// Connect useReducer to Redux DevTools
function useReducerWithDevTools(reducer, initialState, name) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
if (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name });
devTools.init(initialState);
const wrappedDispatch = (action) => {
dispatch(action);
devTools.send(action, reducer(state, action));
};
return () => devTools.disconnect();
}
}, []);
return [state, dispatch];
}
// Usage
function TodoApp() {
const [todos, dispatch] = useReducerWithDevTools(
todoReducer,
[],
'TodoApp'
);
// Now see all actions in Redux DevTools!
// - Time-travel debugging
// - Action history
// - State diff
}
Example: Custom state debugging hooks
// Development-only state logger
function useStateWithLogger(initialState, name) {
const [state, setState] = useState(initialState);
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
console.log(`[${name}] State changed:`, state);
}
}, [state, name]);
return [state, setState];
}
// Track state change reasons
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('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
}
// Usage
function ExpensiveComponent(props) {
useWhyDidYouUpdate('ExpensiveComponent', props);
return <div>...</div>;
}
5. Time-travel Debugging and State History
| Feature | Capability | Tool | Use Case |
|---|---|---|---|
| Profiler Timeline | Scrub through recorded renders | React DevTools Profiler | See component states at different render commits |
| Redux DevTools | Jump to any action in history | Redux DevTools Extension | Time-travel through state changes, replay actions |
| React Query DevTools | View cache history and mutations | @tanstack/react-query-devtools | Debug data fetching and caching issues |
| Custom History | Track component lifecycle events | Custom hooks with logging | Create audit trail of state changes |
| Browser DevTools | Record and replay interactions | Chrome Recorder, Replay.io | Reproduce bugs, share reproductions |
| Error Replay | Record session before errors | LogRocket, FullStory, Sentry | Debug production errors with context |
Example: Using Profiler for time-travel debugging
// Record interaction flow:
// 1. Open React DevTools → Profiler
// 2. Click Record
// 3. Perform user actions (form submit, navigation, etc.)
// 4. Click Stop
// 5. Use timeline scrubber to navigate commits
// Each commit shows:
// - Which components rendered
// - Why they rendered (props/state change)
// - Render duration
// - Component props/state at that moment
// Navigate commits:
// - Left/Right arrows to step through
// - Click specific commit in timeline
// - See exact component state at that point
// - Compare before/after states
Example: Redux DevTools time-travel
// Install Redux DevTools Extension
// Setup store with DevTools
import { createStore } from 'redux';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
// In Redux DevTools panel:
// 1. See all dispatched actions in order
// 2. Click any action to jump to that state
// 3. Use slider to scrub through action history
// 4. Toggle actions on/off to see impact
// 5. Export/import state snapshots
// 6. Replay actions at different speeds
// DevTools features:
// - Diff: See what changed in state
// - Test: Generate test code from actions
// - Export: Save state/action history as JSON
// - Import: Load previous session
// - Persist: Keep state across page reloads
Example: Building custom history tracker
// Create state history hook
function useStateHistory(initialState, maxHistory = 50) {
const [state, setState] = useState(initialState);
const [history, setHistory] = useState([initialState]);
const [currentIndex, setCurrentIndex] = useState(0);
const updateState = useCallback((newState) => {
const value = typeof newState === 'function' ? newState(state) : newState;
setHistory(prev => {
const newHistory = prev.slice(0, currentIndex + 1);
newHistory.push(value);
return newHistory.slice(-maxHistory);
});
setCurrentIndex(prev => Math.min(prev + 1, maxHistory - 1));
setState(value);
}, [state, currentIndex, maxHistory]);
const undo = useCallback(() => {
if (currentIndex > 0) {
const newIndex = currentIndex - 1;
setCurrentIndex(newIndex);
setState(history[newIndex]);
}
}, [currentIndex, history]);
const redo = useCallback(() => {
if (currentIndex < history.length - 1) {
const newIndex = currentIndex + 1;
setCurrentIndex(newIndex);
setState(history[newIndex]);
}
}, [currentIndex, history]);
const canUndo = currentIndex > 0;
const canRedo = currentIndex < history.length - 1;
return {
state,
setState: updateState,
undo,
redo,
canUndo,
canRedo,
history,
currentIndex
};
}
// Usage - Text editor with undo/redo
function TextEditor() {
const {
state: text,
setState: setText,
undo,
redo,
canUndo,
canRedo
} = useStateHistory('');
return (
<div>
<button onClick={undo} disabled={!canUndo}>Undo</button>
<button onClick={redo} disabled={!canRedo}>Redo</button>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
6. Production Debugging and Error Tracking (Source Maps)
| Tool/Service | Capabilities | Key Features | Best For |
|---|---|---|---|
| Sentry | Error tracking, performance monitoring | Error grouping, source maps, breadcrumbs, releases | Production error tracking and alerting |
| LogRocket | Session replay, error tracking | Video replay, network logs, console logs, Redux state | Visual debugging with full session context |
| Bugsnag | Error monitoring and reporting | Stability score, release tracking, user impact analysis | Error management and prioritization |
| Datadog RUM | Real user monitoring | Performance metrics, session replay, error tracking, analytics | Full-stack monitoring and observability |
| React Error Boundary | Catch React errors in production | Fallback UI, error reporting integration | Graceful error handling in app |
| Source Maps | Map minified code to source | Readable stack traces in production | Debug minified production code |
Example: Sentry setup for React
// Install
npm install @sentry/react
// Initialize in entry point
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
integrations: [
new BrowserTracing(),
],
tracesSampleRate: 1.0, // Sample rate for performance
environment: process.env.NODE_ENV,
release: process.env.REACT_APP_VERSION,
// Upload source maps for readable stack traces
beforeSend(event, hint) {
// Filter sensitive data
if (event.request) {
delete event.request.cookies;
}
return event;
}
});
// Wrap app with Sentry ErrorBoundary
function App() {
return (
<Sentry.ErrorBoundary fallback={ErrorFallback} showDialog>
<YourApp />
</Sentry.ErrorBoundary>
);
}
// Add user context
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username
});
// Add breadcrumbs for debugging
Sentry.addBreadcrumb({
category: 'navigation',
message: 'User navigated to dashboard',
level: 'info'
});
// Manual error capture
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: {
section: 'payment',
severity: 'critical'
},
contexts: {
transaction: {
id: transactionId,
amount: amount
}
}
});
}
Example: LogRocket integration
// Install
npm install logrocket logrocket-react
// Initialize
import LogRocket from 'logrocket';
import setupLogRocketReact from 'logrocket-react';
LogRocket.init('YOUR_APP_ID');
setupLogRocketReact(LogRocket);
// Identify users
LogRocket.identify(user.id, {
name: user.name,
email: user.email,
subscriptionType: user.plan
});
// Integration with Sentry
import * as Sentry from '@sentry/react';
LogRocket.getSessionURL(sessionURL => {
Sentry.configureScope(scope => {
scope.setExtra('sessionURL', sessionURL);
});
});
// Track custom events
LogRocket.track('Purchase Completed', {
productId: product.id,
revenue: price,
category: product.category
});
// Sanitize sensitive data
LogRocket.init('YOUR_APP_ID', {
dom: {
inputSanitizer: true, // Sanitize input values
},
network: {
requestSanitizer: request => {
// Remove sensitive headers
if (request.headers['Authorization']) {
request.headers['Authorization'] = 'REDACTED';
}
return request;
},
responseSanitizer: response => {
// Remove sensitive response data
if (response.body && response.body.creditCard) {
response.body.creditCard = 'REDACTED';
}
return response;
}
}
});
Example: Production error boundary with reporting
import { Component } from 'react';
import * as Sentry from '@sentry/react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error caught by boundary:', error, errorInfo);
}
// Report to error tracking service
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack
}
}
});
// Log to analytics
analytics.track('error_boundary_triggered', {
error: error.message,
component: errorInfo.componentStack.split('\n')[1]
});
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div role="alert">
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
</details>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
Example: Source map configuration for production debugging
// Create React App - .env.production
GENERATE_SOURCEMAP=true
// Upload source maps to Sentry
// package.json
{
"scripts": {
"build": "react-scripts build",
"sentry:upload": "sentry-cli releases files $npm_package_version upload-sourcemaps ./build"
}
}
// Webpack config (for custom setup)
module.exports = {
devtool: 'source-map', // Generate source maps
plugins: [
new SentryWebpackPlugin({
include: './build',
ignoreFile: '.sentrycliignore',
ignore: ['node_modules', 'webpack.config.js'],
configFile: 'sentry.properties',
release: process.env.REACT_APP_VERSION
})
]
};
// .sentryignore
node_modules/
*.map
// Only upload source maps, don't serve them publicly
// Serve source maps only to authenticated users or keep them private
// Configure error tracking to fetch maps from your servers
Production Debugging Best Practices: Always use error boundaries, upload source maps to error
tracking service, sanitize sensitive data before logging, add user context to errors, implement breadcrumbs for
error context, monitor error rates and trends, set up alerts for critical errors, test error tracking in
staging, use session replay for complex bugs, correlate errors with releases.