DEV Community

Talisson
Talisson

Posted on

Hooks Under the Hood: How React Hooks Actually Work

React hooks revolutionized how we write components, but how well do you really understand what happens when you call useState or useEffect?

This article peels back the abstraction layer to explore how hooks work internally, including the mechanisms React uses to manage them. Whether you're building custom hooks, debugging tricky bugs, or just curious, this deep dive will make you a better React developer.

Why You Should Care

Hooks are not magic. They rely on pure JavaScript, follow strict rules, and depend on a deterministic call order. Understanding these concepts helps you:

  • Write predictable, bug-free components
  • Debug strange hook behavior
  • Build advanced custom hooks with confidence
  • Understand why the Rules of Hooks exist

The Core Idea

React maintains a linked list of hooks for each component, stored in a "fiber"—React’s internal data structure representing a component during reconciliation. When a component renders, React tracks every hook call in order, using a pointer to manage the list.

Here’s a simplified view of how React tracks hooks:

// Simplified: Represents the fiber's memoizedState linked list
let currentHookIndex = 0;

function useState(initialValue) {
  // Reset index at the start of each render
  if (isNewRender()) currentHookIndex = 0;

  // Get or create hook node
  const hook = hookStore[currentComponent][currentHookIndex] || { state: initialValue };

  const setState = (newValue) => {
    // Support functional updates
    hook.state = typeof newValue === 'function' ? newValue(hook.state) : newValue;
    scheduleRerender(currentComponent); // Batched in real React
  };

  hookStore[currentComponent][currentHookIndex] = hook;
  currentHookIndex++;

  return [hook.state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Each hook is tracked by its index in the render cycle.
  • React expects the number and order of hooks to remain consistent across renders.
  • State updates are batched for performance, not applied immediately.

How React Tracks Hook Calls

React uses a linked list of hook nodes within each component’s fiber. A fiber is React’s internal representation of a component, used for reconciliation and rendering. Each hook (useState, useEffect, etc.) creates a node in this list.

During a render, React:

  1. Sets the currentlyRenderingFiber.
  2. Resets the workInProgressHook pointer to null.
  3. For each hook call, creates or reuses a node in the linked list.
  4. Stores values like state, effect dependencies, or cleanup functions in the node.

The linked list ensures hooks are processed linearly, matching the order of function calls, while persisting state across renders.

What Happens When You Call useState

Consider this code:

const [count, setCount] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Under the hood:

  • React checks the current hook index in the fiber’s memoizedState.
  • On the first render, it stores the initial value (0) in the hook node.
  • setCount is a closure that enqueues an update (supporting functional updates like setCount(prev => prev + 1)) and schedules a re-render.
  • On re-renders, React retrieves the current state from the same node, ignoring the initialValue.

What About useEffect

useEffect manages side effects and works differently:

function useEffect(effect, deps) {
  // Get or create the next hook node
  const hook = getNextHook() || { deps: null, cleanup: null };

  // Shallow equality check for dependencies
  const hasChanged = !areDepsEqual(hook.deps, deps);

  if (hasChanged) {
    // Run cleanup before next effect or unmount
    if (hook.cleanup) hook.cleanup();

    // Schedule effect to run after commit phase
    scheduleEffect(() => {
      hook.cleanup = effect(); // Store cleanup function if returned
    });
  }

  hook.deps = deps;
}

// Simplified shallow equality check
function areDepsEqual(oldDeps, newDeps) {
  if (!oldDeps || !newDeps) return false;
  if (oldDeps.length !== newDeps.length) return false;
  return oldDeps.every((dep, i) => Object.is(dep, newDeps[i]));
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • React stores the effect callback and dependency array in the hook node.
  • After the commit phase (post-paint), React compares dependencies using shallow equality (Object.is).
  • If dependencies change or it’s the first run, React runs the effect.
  • Cleanup functions (if returned) run before the next effect or when the component unmounts.

Why the Rules of Hooks Matter

React relies on consistent hook call order. Conditional hooks break this model, causing mismatches in the linked list.

Bad:

if (user) {
  useEffect(() => {
    // Breaks hook order
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

Good:

useEffect(() => {
  if (!user) return;
  // Safe conditional logic
}, [user]);
Enter fullscreen mode Exit fullscreen mode

Breaking the Rules of Hooks (e.g., calling hooks conditionally or in loops) leads to errors like "Invalid Hook Call" because React can’t match hook nodes across renders.

Common Pitfalls

  • Stale Closures: Missing dependencies in useEffect can lead to stale values. Always include all variables used in the effect in the dependency array.
  • Overusing State: Derived values (e.g., count * 2) don’t always need state. Use computed values when possible to avoid unnecessary renders.
  • Concurrent Rendering: In Concurrent Mode, React may render components multiple times. The hook linked list ensures state consistency, but be aware of effects running more often.

Custom Hooks Use the Same Mechanism

Custom hooks are just functions that call other hooks:

function useDouble(count) {
  // Note: In practice, `count * 2` could be a computed value unless state is needed
  const [double, setDouble] = useState(count * 2);

  useEffect(() => {
    // Avoid redundant updates
    if (double !== count * 2) {
      setDouble(count * 2);
    }
  }, [count, double]);

  return double;
}
Enter fullscreen mode Exit fullscreen mode

Custom hooks register in the same linked list, following the same call order. This allows nesting but requires adherence to the Rules of Hooks.

Recap: The Lifecycle of a Hook Call

  1. Setup Phase: React prepares the fiber and resets the hook pointer.
  2. Execution Phase: Calls the component function, executing hooks in order.
  3. Tracking: Each hook call is tied to its position in the fiber’s linked list.
  4. Commit Phase: React applies side effects (e.g., from useEffect) after painting.
  5. Re-render: State updates trigger the cycle again, reusing the same hook nodes.

Want to Dive Deeper

Explore these resources:

Final Thoughts

Understanding hooks under the hood isn’t just academic. It equips you to debug issues, optimize performance, and build robust components. By grasping the linked list model, call order, and lifecycle, you’ll write more predictable React code and tackle complex problems with confidence.

Top comments (0)