Tutorial6 min read

React Hooks Debugging Guide: useState, useEffect, and Custom Hooks

The most common React hooks bugs explained: stale closures, infinite loops, missing dependencies, and how to debug them without losing your mind.

ReacthooksdebugginguseEffectuseStatestale closureReact debugging

Why React Hooks Bugs Are Different

Regular bugs have a crash line. Hooks bugs usually don't crash — they produce wrong behavior: stale data, infinite loops, missed updates. The error message (when there is one) points at React internals, not your code.

These are the patterns behind 80% of React hooks bugs.


Bug 1: The Stale Closure

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1)  // always uses count = 0 — stale closure
    }, 1000)
    return () => clearInterval(id)
  }, [])  // empty deps — effect captures count at mount time only
}

The effect closes over count at the time it runs. With [] as deps, it only runs once — capturing count = 0 forever. The counter gets stuck at 1.

Fix: use the functional update form.

setCount(prev => prev + 1)  // always has the latest value

Or add count to the dependency array (but that recreates the interval on every tick, which is wasteful for this case).


Bug 2: The Infinite Loop

useEffect(() => {
  setData(processData(data))  // sets state → re-render → effect runs again → infinite
}, [data])

Any time you read state and write to the same state (or derived state) in a useEffect, you risk an infinite loop.

Fix: be specific about what triggers the effect. If you need to process data on load, use an empty dep array and fetch from an external source, not from state.


Bug 3: Object/Array in Dependency Array

useEffect(() => {
  fetchData(filters)
}, [filters])  // filters is an object — new reference on every render

JavaScript compares objects by reference. Even if filters has the same values, a new object literal {} is a new reference — and React sees it as a changed dependency. Effect runs on every render.

Fix: either use primitive values as deps, memoize the object with useMemo, or use useCallback for function deps.

const stableFilters = useMemo(() => filters, [filters.status, filters.page])

Bug 4: Missing Cleanup

useEffect(() => {
  fetchUser(userId).then(data => setUser(data))
  // component unmounts before fetch completes
  // setUser called on unmounted component → memory leak warning
}, [userId])

Fix: use an abort controller or a cancelled flag.

useEffect(() => {
  let cancelled = false
  fetchUser(userId).then(data => {
    if (!cancelled) setUser(data)
  })
  return () => { cancelled = true }
}, [userId])

Bug 5: Custom Hook Returns Wrong Reference

If your custom hook returns a new object or array on every call, components using it will re-render infinitely if that value is used as a dep.

// Bad — new array on every render
function usePermissions() {
  return ['read', 'write']
}

// Good — stable reference
function usePermissions() {
  return useMemo(() => ['read', 'write'], [])
}

How to Debug Hooks Issues Fast

React DevTools — Install the browser extension. The Profiler tab shows which components re-rendered and why. Hooks tab shows current state and effects.

Why Did You Render — Library that logs to the console when a component re-renders unnecessarily. Add it in dev mode only.

DebugAI — For hooks errors that produce actual exceptions (TypeError, ReferenceError), press Ctrl+Shift+D. It reads your component file and its custom hooks to find the root cause across files — without you having to trace it manually.


Install DebugAI — debug React errors with full codebase context

Debug faster starting today.

Free VS Code extension. 10 sessions/day. No credit card.

Install Free →

Related Posts

Tutorial

Fix KeyError in Python: 5 Causes and How to Find the Source

5 min read

Tutorial

Fix IndentationError in Python: 6 Causes and Exact Fixes (2026)

5 min read

← All posts