How to debug React hooks

Debugging React hooks requires understanding hook execution order, dependency arrays, and closure scope. As the creator of CoreUI with 12 years of React development experience, I’ve debugged thousands of hook-related issues in production applications, helping teams identify stale closures, infinite loops, and missing dependencies.

The most effective approach combines React DevTools with strategic console logs and breakpoints.

Use React DevTools

Install React DevTools browser extension, then:

  1. Open React DevTools (F12 → React tab)
  2. Select component in the tree
  3. View hooks in the right panel
  4. See hook values in real-time
function UserProfile() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  // DevTools shows:
  // State: null
  // State: true

  return <div>{loading ? 'Loading...' : user?.name}</div>
}

Log Hook Execution

function useDebugValue(value, label) {
  console.log(`[${label}]`, value)
  return value
}

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

  useEffect(() => {
    console.log('Effect running, count:', count)

    return () => {
      console.log('Effect cleanup, count:', count)
    }
  }, [count])

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Debug Dependencies

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

function MyComponent({ userId, filters }) {
  const prevUserId = usePrevious(userId)
  const prevFilters = usePrevious(filters)

  useEffect(() => {
    console.log('Effect triggered')
    console.log('userId changed:', prevUserId !== userId)
    console.log('filters changed:', prevFilters !== filters)
  }, [userId, filters])

  return <div>Content</div>
}

Track Re-renders

import { useRef } from 'react'

function useRenderCount() {
  const renderCount = useRef(0)
  renderCount.current++
  console.log(`Render #${renderCount.current}`)
  return renderCount.current
}

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
  })
}

function UserCard({ user, theme }) {
  useRenderCount()
  useWhyDidYouUpdate('UserCard', { user, theme })

  return <div>{user.name}</div>
}

Debug Custom Hooks

function useDebugHook(hookName, dependencies) {
  const renderCount = useRef(0)
  renderCount.current++

  console.group(`🪝 ${hookName} - Render #${renderCount.current}`)
  console.log('Dependencies:', dependencies)

  useEffect(() => {
    console.log(`Effect: Running`)

    return () => {
      console.log(`Effect: Cleanup`)
    }
  }, dependencies)

  console.groupEnd()
}

function useFetchUser(userId) {
  useDebugHook('useFetchUser', [userId])

  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    console.log('Fetching user:', userId)

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        console.log('User fetched:', data)
        setUser(data)
        setLoading(false)
      })
  }, [userId])

  return { user, loading }
}

Detect Infinite Loops

function useInfiniteLoopDetector(hookName, maxRenders = 50) {
  const renderCount = useRef(0)
  const resetTimer = useRef(null)

  renderCount.current++

  // Reset counter after 1 second of no renders
  clearTimeout(resetTimer.current)
  resetTimer.current = setTimeout(() => {
    renderCount.current = 0
  }, 1000)

  if (renderCount.current > maxRenders) {
    console.error(
      `🚨 INFINITE LOOP DETECTED in ${hookName}!`,
      `Rendered ${renderCount.current} times in 1 second`
    )
  }
}

function ProblematicComponent() {
  useInfiniteLoopDetector('ProblematicComponent')

  const [count, setCount] = useState(0)

  useEffect(() => {
    // This causes infinite loop!
    setCount(count + 1)
  }) // Missing dependency array

  return <div>Count: {count}</div>
}

Debug useEffect Timing

function useEffectDebugger(effectName, effect, deps) {
  const mountTime = useRef(Date.now())

  useEffect(() => {
    const startTime = Date.now()
    console.log(`[${effectName}] Starting effect`)
    console.log(`Time since mount: ${startTime - mountTime.current}ms`)

    const cleanup = effect()

    return () => {
      const cleanupTime = Date.now()
      console.log(`[${effectName}] Cleanup`)
      console.log(`Effect duration: ${cleanupTime - startTime}ms`)

      if (cleanup) cleanup()
    }
  }, deps)
}

function TimedComponent() {
  useEffectDebugger('Fetch Data', () => {
    console.log('Fetching...')

    return () => {
      console.log('Cancelling fetch')
    }
  }, [])

  return <div>Content</div>
}

Use React DevTools Profiler

import { Profiler } from 'react'

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  console.log({
    component: id,
    phase, // "mount" or "update"
    renderTime: actualDuration,
    totalTime: baseDuration
  })
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <UserDashboard />
    </Profiler>
  )
}

Best Practice Note

This is the debugging workflow we use across all CoreUI React projects. React DevTools shows hook state in real-time, while strategic console logs reveal execution order and dependency changes. Always use useEffect dependency arrays correctly, enable ESLint’s exhaustive-deps rule to catch missing dependencies, and use custom debug hooks to track re-renders and identify performance issues.

For production applications, consider using CoreUI’s React Admin Template which includes debugging utilities and performance monitoring.

For related debugging techniques, check out how to debug React with breakpoints and how to fix stale closures in React hooks.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team