flowchart LR
A["Select Component"] --> B["View Props"]
A --> C["View State"]
A --> D["View Hooks"]
B --> E["Trace to Source"]
C --> F["Edit Live"]
D --> G["See Dependencies"]
Debugging Modern React: From Console to Profiler
Bugs are inevitable. What separates senior engineers from juniors isn’t avoiding bugs—it’s finding them fast. This chapter covers the modern debugging toolkit for React applications.
React DevTools: Your Primary Weapon
Install the React DevTools browser extension. It adds two tabs to your browser’s developer tools:
Components Tab
Inspect the component tree, view props and state, and trace where data comes from.
Pro Tips:
- Click any component → See its props, state, and hooks in the right panel
- Search by name → Type component name to find it instantly
- “Rendered by” → Trace the parent chain to understand data flow
- Edit state live → Double-click state values to modify and test
The Profiler: Finding Performance Bottlenecks
The Profiler tab records renders and shows you exactly why components re-rendered.
Recording a Session
- Open Profiler tab
- Click “Record” button
- Interact with your app
- Click “Stop”
- Analyze the flame graph
Reading the Flame Graph
| Color | Meaning |
|---|---|
| Gray | Did not render |
| Blue-Green | Fast render (< 1ms) |
| Yellow | Medium render (1-16ms) |
| Red | Slow render (> 16ms) — Optimize this! |
Why Did This Render?
Enable “Record why each component rendered” in Profiler settings. Common causes:
- Props changed — Parent passed new reference
- State changed — Component’s own state updated
- Context changed — Provider value changed
- Parent rendered — No memoization
Console Debugging Patterns
Strategic Logging
// ❌ Bad: Noise
console.log(data);
// ✅ Good: Context
console.log('[UserProfile] Fetched user:', { userId, data });
// ✅ Better: Grouped
console.group('Auth Flow');
console.log('Token:', token);
console.log('User:', user);
console.groupEnd();
// ✅ Best: Conditional in dev only
if (import.meta.env.DEV) {
console.log('[Debug] State update:', state);
}
useDebugValue for Custom Hooks
Make your custom hooks show meaningful info in DevTools:
function useAuth() {
const [user, setUser] = useState<User | null>(null);
// Shows "Authenticated: john@example.com" or "Not authenticated" in DevTools
useDebugValue(user ? `Authenticated: ${user.email}` : 'Not authenticated');
return { user, login, logout };
}
Debugging State Issues
The “Why Did My State Disappear?” Pattern
// ❌ Common mistake: Setting state during render
function BadComponent({ items }) {
const [filtered, setFiltered] = useState([]);
// 🔥 This causes infinite loops!
setFiltered(items.filter(i => i.active));
return <List items={filtered} />;
}
// ✅ Fix: Use useMemo for derived state
function GoodComponent({ items }) {
const filtered = useMemo(
() => items.filter(i => i.active),
[items]
);
return <List items={filtered} />;
}
Stale Closure Debugging
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ❌ Bug: count is always 0 (stale closure)
console.log('Count is:', count);
setCount(count + 1); // Always sets to 1
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps = stale closure
// ✅ Fix: Use functional update
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // Always uses latest
}, 1000);
return () => clearInterval(interval);
}, []);
}
Network Debugging
Inspecting API Calls
- Open Network tab in DevTools
- Filter by Fetch/XHR
- Click any request to see:
- Headers — Auth tokens, content types
- Payload — What you sent
- Response — What came back
- Timing — Where time was spent
Debugging with TanStack Query DevTools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
{/* Floating button in dev mode */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
The Query DevTools show: - All cached queries - Query status (fresh, stale, fetching) - Cache timing - Manual refetch/invalidate buttons
Source Maps & Error Boundaries
Production Debugging with Source Maps
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // Generate source maps
},
});Source maps expose your original code. Options: - Hidden source maps — Upload to error tracking only - Separate hosting — Serve maps from authenticated endpoint
Error Boundary with Reporting
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
// Report to your error tracking service
useEffect(() => {
reportError(error);
}, [error]);
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<YourApp />
</ErrorBoundary>
);
}
AI-Assisted Debugging
Modern workflows include AI as a debugging partner:
## Prompt Template for Debugging
I have a React component that [describe behavior].
Expected: [what should happen]
Actual: [what happens instead]
Here's the relevant code:
```tsx
[paste component]I’ve tried: [what you’ve checked]
What could cause this? ```
When to use AI: - Unfamiliar error messages - Complex state interactions - Performance issues you can’t pinpoint
When NOT to use AI: - Before reading the actual error message - Without reproducing the bug yourself first - For trivial console.log placement
Debugging Checklist
The best debuggers aren’t those who never encounter bugs—they’re the ones who find them in minutes, not hours.