React Debugging Guide
A systematic approach to debugging React applications, covering common error patterns, modern debugging tools, and step-by-step resolution strategies.
Common Error Patterns
- "Cannot read property of undefined" / "TypeError: X is undefined"
Cause: Accessing properties on null/undefined values, often from:
-
Uninitialized state
-
API data not yet loaded
-
Incorrect prop drilling
Solutions:
// Problem: Accessing nested property before data loads const name = user.profile.name; // Error if user is undefined
// Solution 1: Optional chaining const name = user?.profile?.name;
// Solution 2: Default values const name = user?.profile?.name ?? 'Unknown';
// Solution 3: Early return pattern if (!user) return <LoadingSpinner />; return <div>{user.profile.name}</div>;
// Solution 4: Initialize state properly const [user, setUser] = useState({ profile: { name: '' } });
- Infinite Re-render Loops ("Too many re-renders")
Cause: State updates triggering renders that trigger more state updates.
Common Triggers:
-
Calling setState directly in render
-
useEffect with missing/incorrect dependencies
-
Object/array references changing every render
Solutions:
// Problem: setState in render function BadComponent() { const [count, setCount] = useState(0); setCount(count + 1); // Infinite loop! return <div>{count}</div>; }
// Solution: Move to useEffect or event handler function GoodComponent() { const [count, setCount] = useState(0); useEffect(() => { setCount(c => c + 1); }, []); // Run once on mount return <div>{count}</div>; }
// Problem: Object dependency causing infinite loop useEffect(() => { fetchData(options); }, [options]); // New object reference every render!
// Solution: useMemo for stable reference const memoizedOptions = useMemo(() => ({ page: 1 }), []); useEffect(() => { fetchData(memoizedOptions); }, [memoizedOptions]);
- Stale Closure in Hooks
Cause: Callbacks capture old values from previous renders.
Solutions:
// Problem: Stale closure in interval function Timer() { const [count, setCount] = useState(0);
useEffect(() => { const id = setInterval(() => { console.log(count); // Always logs initial value! setCount(count + 1); // Only increments once }, 1000); return () => clearInterval(id); }, []); // Empty deps = stale closure }
// Solution 1: Functional update setCount(prevCount => prevCount + 1);
// Solution 2: useRef for mutable values const countRef = useRef(count); useEffect(() => { countRef.current = count; }, [count]);
// Solution 3: Include dependency (if appropriate) useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]); // Re-creates interval on each count change
// Solution 4: useEffectEvent (React 19.2+) const onTick = useEffectEvent(() => { setCount(count + 1); // Always has fresh count }); useEffect(() => { const id = setInterval(onTick, 1000); return () => clearInterval(id); }, []);
- Key Prop Warnings
Cause: Missing or non-unique keys in lists.
Solutions:
// Problem: No key {items.map(item => <Item data={item} />)}
// Problem: Index as key (causes issues with reordering) {items.map((item, index) => <Item key={index} data={item} />)}
// Solution: Stable unique identifier {items.map(item => <Item key={item.id} data={item} />)}
// For items without IDs, generate stable keys
{items.map(item => <Item key={${item.name}-${item.date}} data={item} />)}
- Memory Leaks with useEffect
Cause: Subscriptions, timers, or async operations not cleaned up.
Solutions:
// Problem: No cleanup useEffect(() => { const subscription = dataSource.subscribe(handleChange); // Memory leak when component unmounts! }, []);
// Solution: Return cleanup function useEffect(() => { const subscription = dataSource.subscribe(handleChange); return () => subscription.unsubscribe(); }, []);
// Problem: Async operation after unmount useEffect(() => { fetchData().then(data => { setData(data); // Error if unmounted! }); }, []);
// Solution: AbortController for fetch useEffect(() => { const controller = new AbortController();
fetchData({ signal: controller.signal }) .then(data => setData(data)) .catch(err => { if (err.name !== 'AbortError') throw err; });
return () => controller.abort(); }, []);
// Solution: Ignore flag for other async useEffect(() => { let ignore = false;
fetchData().then(data => { if (!ignore) setData(data); });
return () => { ignore = true; }; }, []);
- Hydration Mismatches (SSR/SSG)
Cause: Server-rendered HTML differs from client-side React.
Common Triggers:
-
Using Date.now() , Math.random() in render
-
Browser-only APIs (window, localStorage)
-
Conditional rendering based on client state
Solutions:
// Problem: Random value differs server vs client function BadComponent() { return <div>{Math.random()}</div>; // Hydration mismatch! }
// Solution 1: useEffect for client-only values function GoodComponent() { const [randomValue, setRandomValue] = useState(null);
useEffect(() => { setRandomValue(Math.random()); }, []);
return <div>{randomValue}</div>; }
// Solution 2: suppressHydrationWarning (use sparingly) <time suppressHydrationWarning>{new Date().toISOString()}</time>
// Solution 3: Client-only component const ClientOnlyComponent = dynamic( () => import('./ClientComponent'), { ssr: false } );
- Hook Rules Violations
Error: "React Hook useXXX is called conditionally"
Cause: Hooks called inside conditions, loops, or after early returns.
Solutions:
// Problem: Conditional hook function BadComponent({ shouldFetch }) { if (shouldFetch) { useEffect(() => fetchData(), []); // Error! } }
// Solution: Condition inside hook function GoodComponent({ shouldFetch }) { useEffect(() => { if (shouldFetch) fetchData(); }, [shouldFetch]); }
// Problem: Hook after early return function BadComponent({ data }) { if (!data) return null; const [state, setState] = useState(data); // Error! }
// Solution: Move hooks before returns function GoodComponent({ data }) { const [state, setState] = useState(data); if (!data) return null; return <div>{state}</div>; }
- Missing Dependencies Warning
Error: "React Hook has a missing dependency: 'XXX'"
Solutions:
// Problem: Missing dependency
const [count, setCount] = useState(0);
useEffect(() => {
document.title = Count: ${count};
}, []); // Warning: missing 'count'
// Solution 1: Add the dependency
useEffect(() => {
document.title = Count: ${count};
}, [count]);
// Solution 2: Remove if truly not needed (rare) // eslint-disable-next-line react-hooks/exhaustive-deps
// Solution 3: useCallback for function dependencies const handleClick = useCallback(() => { console.log(count); }, [count]);
useEffect(() => { element.addEventListener('click', handleClick); return () => element.removeEventListener('click', handleClick); }, [handleClick]);
Debugging Tools
React Developer Tools
The official browser extension for debugging React applications.
Installation:
-
Chrome: React Developer Tools
-
Firefox: React Developer Tools
Key Features:
Components Tab:
- Inspect component tree hierarchy
- View and edit props in real-time
- View and modify state
- Search components by name
- View component source location
Profiler Tab:
- Record render performance
- Identify slow components
- View render timing flamegraph
- Detect unnecessary re-renders
Pro Tips:
// Name components for easier debugging const MyComponent = () => <div />; MyComponent.displayName = 'MyComponent';
// Or use named exports export function MyComponent() { ... }
Console.log Debugging
Strategic logging patterns for React:
// Log props and state changes function DebugComponent({ data }) { console.log('[DebugComponent] render', { data });
useEffect(() => { console.log('[DebugComponent] effect triggered', { data }); return () => console.log('[DebugComponent] cleanup'); }, [data]);
return <div>{data}</div>; }
// Log render counts
function RenderCounter({ name }) {
const renderCount = useRef(0);
renderCount.current++;
console.log([${name}] render #${renderCount.current});
return null;
}
// Conditional logging const DEBUG = process.env.NODE_ENV === 'development'; DEBUG && console.log('Debug info:', data);
Error Boundaries
Catch and handle errors in component trees:
class ErrorBoundary extends React.Component { state = { hasError: false, error: null };
static getDerivedStateFromError(error) { return { hasError: true, error }; }
componentDidCatch(error, errorInfo) { console.error('Error caught:', error, errorInfo); // Log to error monitoring service logErrorToService(error, errorInfo); }
render() { if (this.state.hasError) { return ( <div className="error-fallback"> <h2>Something went wrong</h2> <details> <summary>Error details</summary> <pre>{this.state.error?.toString()}</pre> </details> <button onClick={() => this.setState({ hasError: false })}> Try again </button> </div> ); } return this.props.children; } }
// Usage <ErrorBoundary> <RiskyComponent /> </ErrorBoundary>
React Strict Mode
Helps identify potential problems:
// In index.js or App.js <React.StrictMode> <App /> </React.StrictMode>
What it detects:
-
Unsafe lifecycle methods
-
Legacy string refs
-
Unexpected side effects (double-invokes effects in dev)
-
Deprecated APIs
why-did-you-render
Track unnecessary re-renders:
npm install @welldone-software/why-did-you-render
// wdyr.js - import before React import React from 'react';
if (process.env.NODE_ENV === 'development') { const whyDidYouRender = require('@welldone-software/why-did-you-render'); whyDidYouRender(React, { trackAllPureComponents: true, }); }
// Component to track function MyComponent() { ... } MyComponent.whyDidYouRender = true;
Redux DevTools
For Redux state management debugging:
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({ reducer: rootReducer, devTools: process.env.NODE_ENV !== 'production', });
The Four Phases of React Debugging
Phase 1: Reproduce
Goal: Consistently reproduce the issue.
Checklist: [ ] Can you reproduce in development mode? [ ] Can you reproduce in production build? [ ] What are the exact steps? [ ] Does it happen on initial load or after interaction? [ ] Is it intermittent or consistent? [ ] Which browsers/devices affected?
Minimal Reproduction:
// Create minimal component that shows the bug function BugReproduction() { // Minimal state/props needed const [data, setData] = useState(null);
// Minimal effect/logic useEffect(() => { fetchData().then(setData); }, []);
// Minimal render return <div>{data?.value}</div>; }
Phase 2: Isolate
Goal: Narrow down the source of the problem.
Techniques:
- Binary search: Comment out half the code
- Component isolation: Test component in isolation
- Prop drilling check: Verify props at each level
- State inspection: Log state changes
- Effect tracking: Log effect execution
// Isolation wrapper for debugging function DebugWrapper({ children }) { console.log('[DebugWrapper] children:', children); return ( <ErrorBoundary> {children} </ErrorBoundary> ); }
// Test component in isolation <DebugWrapper> <SuspectedComponent testProp="hardcoded" /> </DebugWrapper>
Phase 3: Diagnose
Goal: Understand why the bug occurs.
Questions to answer:
- Is it a render issue or state issue?
- Is it a timing issue (async, race condition)?
- Is it a reference equality issue?
- Is it a missing dependency?
- Is it a stale closure?
- Is it a side effect issue?
// Diagnostic hooks function useDiagnostic(name, value) { const prevValue = useRef(value); const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
console.log([${name}] Render #${renderCount.current});
console.log( Previous:, prevValue.current);
console.log( Current:, value);
console.log( Changed:, prevValue.current !== value);
prevValue.current = value;
});
}
// Usage function MyComponent({ data }) { useDiagnostic('MyComponent.data', data); // ... }
Phase 4: Fix and Verify
Goal: Implement fix and prevent regression.
Verification steps:
- Does the fix resolve the original issue?
- Does it introduce new issues?
- Does it work across all affected scenarios?
- Are there edge cases?
- Is there a test to prevent regression?
// Write a test for the bug describe('BugFix', () => { it('should not crash when data is null', () => { render(<MyComponent data={null} />); expect(screen.queryByText('Error')).not.toBeInTheDocument(); });
it('should handle rapid state updates', async () => { const { rerender } = render(<MyComponent count={0} />); for (let i = 1; i <= 100; i++) { rerender(<MyComponent count={i} />); } expect(screen.getByText('100')).toBeInTheDocument(); }); });
Quick Reference Commands
React Testing Library Debug
import { screen } from '@testing-library/react';
// Print current DOM screen.debug();
// Debug specific element screen.debug(screen.getByRole('button'));
// Increase output limit screen.debug(undefined, 30000);
// Log accessible roles screen.logTestingPlaygroundURL();
Performance Profiling
// React Profiler component import { Profiler } from 'react';
function onRenderCallback(
id, // Component name
phase, // "mount" | "update"
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log([Profiler ${id}], { phase, actualDuration });
}
<Profiler id="Navigation" onRender={onRenderCallback}> <Navigation /> </Profiler>
Quick Fixes Checklist
Component not updating? [ ] Check if state is mutated vs replaced [ ] Verify useEffect dependencies [ ] Check for stale closures [ ] Verify key props on lists
Infinite loop? [ ] Check for setState in render [ ] Check useEffect dependencies [ ] Look for object/array reference changes
Memory leak warning? [ ] Add cleanup to useEffect [ ] Cancel async operations [ ] Unsubscribe from events
Hydration error? [ ] Check for browser-only code [ ] Verify consistent server/client output [ ] Use useEffect for client-only values
Useful Browser Console Commands
// Find React fiber $r // Selected component in React DevTools
// Access React internals (dev only) REACT_DEVTOOLS_GLOBAL_HOOK
// Get component state $r.memoizedState
// Get component props $r.memoizedProps
// Force update selected component $r.forceUpdate?.()
Security Considerations (2025)
React Server Components Vulnerability (CVE-2025-XXXXX):
-
Affects React 19.0.0 through 19.2.2
-
Malicious HTTP requests can cause infinite loops
-
Upgrade to 19.0.3, 19.1.4, or 19.2.3+
Check your React version
npm list react
Upgrade if affected
npm install react@latest react-dom@latest
Additional Resources
-
React Developer Tools
-
Common Beginner Mistakes - Josh Comeau
-
React Error Handling Best Practices
-
LogRocket: Common React Errors
-
Zipy: React Debugging Tools