TechLead
Lesson 10 of 20
5 min read
DevTools & Productivity

Debugging React

Debug React applications with React DevTools, component profiling, hooks inspection, and common bug patterns

React DevTools

React Developer Tools is a browser extension that adds two panels to Chrome DevTools: the Components panel and the Profiler panel. These tools give you deep visibility into your React component tree, props, state, hooks, and rendering performance. Install it from the Chrome Web Store.

React DevTools Features

  • Component Tree: Browse the full component hierarchy. Click any component to inspect its props, state, and hooks.
  • Search Components: Search for components by name. Useful in large applications with hundreds of components.
  • Edit Props/State: Double-click any prop or state value to edit it live in the browser.
  • Highlight Updates: Toggle "Highlight updates when components render" to see which components re-render and how often.
  • Profiler: Record rendering performance and see exactly which components rendered, why, and how long each took.

Inspecting Components

Select any component in the Components panel to see its current state in the right sidebar. For function components with hooks, you will see each hook listed (State, Effect, Memo, Callback, Ref, Context) with its current value.

// Given this component:
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const theme = useContext(ThemeContext);
  const memoizedValue = useMemo(() => computeExpensive(user), [user]);

  useEffect(() => {
    fetchUser(userId).then(setUser).finally(() => setLoading(false));
  }, [userId]);

  // ...
}

// In React DevTools Components panel, you'll see:
// hooks:
//   State: { name: "Alice", email: "alice@example.com" }
//   State: false (loading)
//   Context: { mode: "dark", primaryColor: "#3b82f6" }
//   Memo: "computed result"
//   Effect: (shows dependencies: [userId])

Profiling React Performance

The React Profiler records each "commit" (a batch of renders) and shows you exactly which components rendered and why. This is the definitive tool for finding unnecessary re-renders.

Using the Profiler

  1. Open React DevTools > Profiler tab.
  2. Click the record button (blue circle).
  3. Perform the interaction you want to analyze.
  4. Click stop. The flamegraph shows each commit.

Reading the Flamegraph

  • Yellow/Orange bars: Components that rendered in this commit. Width indicates render time.
  • Gray bars: Components that did NOT render (they were memoized or had no changes).
  • "Why did this render?": Enable in Profiler settings. Shows the reason (state change, props change, parent render, context change).

Common React Bugs and How to Debug Them

1. Stale Closures in Hooks

// BUG: Stale closure - count is always 0 inside the interval
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // Always logs 0! Stale closure.
      setCount(count + 1); // Always sets to 1!
    }, 1000);
    return () => clearInterval(id);
  }, []); // Empty deps = closure captures initial count (0)

  return <div>{count}</div>;
}

// FIX: Use functional updater to always get fresh state
function CounterFixed() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(prev => prev + 1); // Uses latest state
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
}

2. Infinite Re-render Loops

// BUG: Object created on every render triggers useEffect infinitely
function UserList() {
  const [users, setUsers] = useState([]);
  const filters = { status: 'active', role: 'admin' }; // New object every render!

  useEffect(() => {
    fetchUsers(filters).then(setUsers);
  }, [filters]); // filters is a new reference each render => infinite loop!

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// FIX: Memoize the object or use primitive dependencies
function UserListFixed() {
  const [users, setUsers] = useState([]);
  const filters = useMemo(() => ({ status: 'active', role: 'admin' }), []);

  useEffect(() => {
    fetchUsers(filters).then(setUsers);
  }, [filters]); // Stable reference, no loop

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

3. Unnecessary Re-renders

// BUG: ExpensiveChild re-renders every time Parent renders
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Click: {count}</button>
      <ExpensiveChild items={getItems()} /> {/* New array ref each render */}
    </div>
  );
}

// FIX: Memoize the child and its props
const MemoizedExpensiveChild = React.memo(ExpensiveChild);

function ParentFixed() {
  const [count, setCount] = useState(0);
  const items = useMemo(() => getItems(), []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Click: {count}</button>
      <MemoizedExpensiveChild items={items} />
    </div>
  );
}

Debugging with React Strict Mode

React Strict Mode (enabled by default in Next.js) intentionally double-invokes certain functions during development to help you find bugs:

  • Components render twice to detect side effects in the render phase.
  • Effects run setup and cleanup an extra time to verify cleanup logic works correctly.
  • Deprecated APIs trigger console warnings.
// If you see double console.logs, Strict Mode is working correctly.
// This is development-only behavior - production builds are not affected.
// DO NOT disable Strict Mode to "fix" the double rendering.

// If Strict Mode reveals a bug (e.g., side effect in render), fix the bug:
function BuggyComponent() {
  // BAD: Side effect in render (Strict Mode exposes this)
  document.title = 'Updated!'; // Runs twice in Strict Mode

  // GOOD: Side effect in useEffect
  useEffect(() => {
    document.title = 'Updated!';
  }, []);
}

React Debugging Checklist

  • Unexpected re-renders? Enable "Highlight updates" in React DevTools. Use the Profiler to find the cause.
  • State not updating? Check if you are mutating state instead of creating new references. Use the Components panel to verify state values.
  • Effect running too often? Check the dependency array. Use ESLint rule react-hooks/exhaustive-deps to catch missing dependencies.
  • Context causing re-renders? Split contexts by update frequency. Use useMemo on context values.

Continue Learning