debugging-react-hooks

SkillWeb & browsing

Debug React hooks infinite loops, stale closures, and missing dependencies. Systematic approach.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the debugging-react-hooks skill

What this skill tells your AI

The instructions your AI receives, as published by porcupine-md/anoa-browser in .claude/skills/library/frontend/debugging-react-hooks/SKILL.md and read by ahel’s review.

The Most Common Hook Bugs

Bug 1: Infinite useEffect Loop

Symptom: Component re-renders endlessly, console floods with logs.

Cause Pattern:

// BUG: object/array created in render → new reference every render
useEffect(() => {
  setData(processData(rawData));
}, [processData]); // processData re-created every render!

Fix:

// Move stable values outside component, or use useCallback/useMemo
const processData = useCallback((data) => {
  return data.map(item => ({ ...item, processed: true }));
}, []); // empty deps = stable reference

useEffect(() => {
  setData(processData(rawData));
}, [processData, rawData]);

Bug 2: Stale Closure

Symptom: Effect reads old state value. The count is always 0 in a timer.

// BUG: count is captured at effect creation time (0)
useEffect(() => {
  const id = setInterval(() => {
    console.log(count); // always 0!
    setCount(count + 1); // always 1!
  }, 1000);
  return () => clearInterval(id);
}, []); // empty deps = stale closure

Fix:

// Option A: functional update (doesn't need count in deps)
useEffect(() => {
  const id = setInterval(() => {
    setCount(prev => prev + 1); // reads current value
  }, 1000);
  return () => clearInterval(id);
}, []);

// Option B: include count in deps (but re-registers interval)
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, [count]);

Bug 3: Missing Dependency Warning → Incorrect Fix

Bad fix:

// Silencing ESLint by disabling rule — WRONG
useEffect(() => {
  fetchUser(userId); // depends on userId but it's not in deps
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Correct fix:

useEffect(() => {
  fetchUser(userId);
}, [userId]); // proper dependency

Debugging Checklist

  1. Add console.log to identify the loop:

    useEffect(() => {
      console.log('Effect ran, deps:', dep1, dep2);
    }, [dep1, dep2]);
    
  2. Check if a dep is an object/array:

    // Each render creates a new object! → new reference → triggers effect
    const options = { limit: 10 }; // bad
    const options = useMemo(() => ({ limit: 10 }), []); // good
    
  3. React DevTools Profiler:

    • "Highlight updates when components render" — shows what's re-rendering
  4. Why Did You Render library:

    npm install @welldone-software/why-did-you-render
    

Rules of Hooks Compliance

  • Only call hooks at the top level (not inside loops/conditions)
  • Only call hooks from React functions
  • Install eslint-plugin-react-hooks — let the linter catch issues

Signals

GitHub stars
23
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
debugging-react-hooks
Source
github.com/porcupine-md/anoa-browser