HardHook Patterns🐛 Debug Challenge

useEffect infinite loop — object in deps

Buggy Code — Can you spot the issue?

let effectCount = 0;

function simulateRender(getOptions, effect) {
  // Simulates component rendering 5 times with deps check
  let prevDeps = null;
  for (let i = 0; i < 5; i++) {
    const options = getOptions(); // gets deps
    const changed = !prevDeps || options !== prevDeps;
    if (changed) {
      effect();
      effectCount++;
    }
    prevDeps = options;
  }
}

// Bug: getOptions creates a new object every "render"
simulateRender(
  () => ({ timeout: 5000, retries: 3 }), // new {} each call!
  () => {} // fetch effect
);

console.log(effectCount); // should be 1, is 5

Fixed Code

let effectCount = 0;

function simulateRender(getOptions, effect) {
  let prevDeps = null;
  for (let i = 0; i < 5; i++) {
    const options = getOptions();
    const changed = !prevDeps || options !== prevDeps;
    if (changed) {
      effect();
      effectCount++;
    }
    prevDeps = options;
  }
}

// Fix: stable reference — created once outside the render
const stableOptions = { timeout: 5000, retries: 3 };
simulateRender(
  () => stableOptions, // same reference every time
  () => {}
);

console.log(effectCount);

Bug Explained

Bug: An inline object literal {} creates a new reference on every call. Each render gets a new options object, so it's never === prevDeps, so the effect runs every time.

Explanation: stableOptions is the same reference on every render. The first render detects a change (null !== stableOptions), effect runs once. Subsequent renders: stableOptions === stableOptions, skip.

Key Insight: Never put inline objects/arrays in useEffect deps: useEffect(() => {}, [{}]) — infinite loop. Either move them outside the component, inside the effect, or wrap in useMemo.

Practice spotting bugs live →

38 debug challenges with AI hints

🐛 Try Debug Lab