Skip to content

Latest commit

 

History

History
765 lines (613 loc) · 23.2 KB

File metadata and controls

765 lines (613 loc) · 23.2 KB
layout default
title Chapter 6: Hooks Implementation
parent React Fiber Internals
nav_order 6

Chapter 6: Hooks Implementation

Welcome to Chapter 6: Hooks Implementation. In this part of React Fiber Internals, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.

Understanding how React implements hooks internally using linked lists and the fiber's memoizedState.

Overview

Hooks are functions that let you "hook into" React state and lifecycle features from function components. Internally, hooks are stored as a linked list on the fiber's memoizedState property.

Hook Architecture

Hook Storage Structure

┌─────────────────────────────────────────────────────────────────┐
│                    Fiber and Hooks                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Fiber                                                         │
│   ┌─────────────────────────────────────────────────────┐       │
│   │  tag: FunctionComponent                             │       │
│   │  type: MyComponent                                  │       │
│   │  memoizedState: ─────────────────┐                  │       │
│   │  updateQueue: ─────────┐         │                  │       │
│   └────────────────────────┼─────────┼──────────────────┘       │
│                            │         │                          │
│                            ▼         ▼                          │
│   UpdateQueue          Hook Linked List                         │
│   ┌───────────┐        ┌─────────┐    ┌─────────┐               │
│   │lastEffect │───────▶│ Hook 1  │───▶│ Hook 2  │──▶ ...       │
│   └───────────┘        │useState │    │useEffect│               │
│                        └─────────┘    └─────────┘               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Hook Object Structure

// Each hook is an object in the linked list
interface Hook {
  memoizedState: any;          // Current state value
  baseState: any;              // State before pending updates
  baseQueue: Update | null;    // First update in queue
  queue: UpdateQueue | null;   // Update queue for this hook
  next: Hook | null;           // Next hook in the list
}

// Example for useState:
const useStateHook = {
  memoizedState: 0,            // Current count value
  baseState: 0,                // Base state
  baseQueue: null,
  queue: {
    pending: null,             // Circular list of updates
    dispatch: boundDispatch,   // dispatch function
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: 0,
  },
  next: null,                  // Points to next hook
};

The Dispatcher

Dispatcher Pattern

// React uses different dispatchers for mount vs update
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  useReducer: mountReducer,
  useCallback: mountCallback,
  useMemo: mountMemo,
  useRef: mountRef,
  useContext: readContext,
  // ... more hooks
};

const HooksDispatcherOnUpdate = {
  useState: updateState,
  useEffect: updateEffect,
  useReducer: updateReducer,
  useCallback: updateCallback,
  useMemo: updateMemo,
  useRef: updateRef,
  useContext: readContext,
  // ... more hooks
};

// The current dispatcher is switched during render
let ReactCurrentDispatcher = {
  current: null,
};

function renderWithHooks(current, workInProgress, Component, props, lanes) {
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;

  // Select dispatcher based on mount vs update
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  // Call the component
  let children = Component(props);

  // Cleanup
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  currentlyRenderingFiber = null;

  return children;
}

useState Implementation

mountState (First Render)

function mountState(initialState) {
  // Create a new hook
  const hook = mountWorkInProgressHook();

  // Initialize state
  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;

  // Create update queue
  const queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;

  // Create dispatch function bound to this fiber and queue
  const dispatch = (queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue
  ));

  return [hook.memoizedState, dispatch];
}

// Helper to create a new hook
function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // First hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the list
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

updateState (Re-renders)

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer(reducer, initialArg) {
  // Get the corresponding hook from previous render
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  const current = currentHook;
  let baseQueue = current.baseQueue;

  // Check for pending updates
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // Merge pending queue with base queue
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    // Process the queue
    const first = baseQueue.next;
    let newState = current.baseState;
    let update = first;

    do {
      // Apply each update
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
    hook.baseState = newState;
    queue.lastRenderedState = newState;
  }

  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

function updateWorkInProgressHook() {
  // Get the next hook from the current tree
  let nextCurrentHook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    nextCurrentHook = current !== null ? current.memoizedState : null;
  } else {
    nextCurrentHook = currentHook.next;
  }

  // Create work-in-progress hook
  let nextWorkInProgressHook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // Reuse existing work-in-progress hook
    workInProgressHook = nextWorkInProgressHook;
    currentHook = nextCurrentHook;
  } else {
    // Clone from current hook
    currentHook = nextCurrentHook;

    const newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }

  return workInProgressHook;
}

dispatchSetState

function dispatchSetState(fiber, queue, action) {
  // Get the update lane
  const lane = requestUpdateLane(fiber);

  // Create update object
  const update = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };

  // Optimization: compute state eagerly if possible
  if (fiber.lanes === NoLanes && (fiber.alternate === null || fiber.alternate.lanes === NoLanes)) {
    const lastRenderedReducer = queue.lastRenderedReducer;
    if (lastRenderedReducer !== null) {
      const currentState = queue.lastRenderedState;
      const eagerState = lastRenderedReducer(currentState, action);
      update.hasEagerState = true;
      update.eagerState = eagerState;

      if (Object.is(eagerState, currentState)) {
        // State didn't change - bail out early
        return;
      }
    }
  }

  // Enqueue the update (circular linked list)
  const pending = queue.pending;
  if (pending === null) {
    update.next = update; // Circular: points to itself
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;

  // Schedule re-render
  scheduleUpdateOnFiber(fiber, lane);
}

useEffect Implementation

Effect Structure

// Effects are stored differently from state hooks
interface Effect {
  tag: HookFlags;              // Effect type flags
  create: () => (() => void) | void;  // Setup function
  destroy: (() => void) | void;       // Cleanup function
  deps: Array<mixed> | null;          // Dependency array
  next: Effect;                       // Next effect (circular list)
}

// Effect flags
const HookPassive = 0b0001;    // useEffect
const HookLayout = 0b0010;     // useLayoutEffect
const HookInsertion = 0b0100;  // useInsertionEffect
const HasEffect = 0b1000;      // Effect should run

mountEffect

function mountEffect(create, deps) {
  return mountEffectImpl(
    PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps
  );
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = mountWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;

  // Mark fiber for passive effects
  currentlyRenderingFiber.flags |= fiberFlags;

  // Store effect on hook and in effect list
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined, // No cleanup yet
    nextDeps
  );
}

function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null,
  };

  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;

  if (componentUpdateQueue === null) {
    // Create effect list
    componentUpdateQueue = { lastEffect: null };
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect; // Circular
  } else {
    // Append to existing effect list
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

updateEffect

function updateEffect(create, deps) {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;

    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // Dependencies haven't changed - skip effect
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  // Dependencies changed - mark effect for execution
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps
  );
}

function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) {
    return false;
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }

  return true;
}

useMemo and useCallback

useMemo Implementation

function mountMemo(nextCreate, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;

  // Compute and store the value
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];

  return nextValue;
}

function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null && nextDeps !== null) {
    const prevDeps = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // Dependencies haven't changed - return cached value
      return prevState[0];
    }
  }

  // Dependencies changed - recompute
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];

  return nextValue;
}

useCallback Implementation

function mountCallback(callback, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;

  // Store the callback itself (not the result)
  hook.memoizedState = [callback, nextDeps];

  return callback;
}

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null && nextDeps !== null) {
    const prevDeps = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // Dependencies haven't changed - return cached callback
      return prevState[0];
    }
  }

  // Dependencies changed - return new callback
  hook.memoizedState = [callback, nextDeps];

  return callback;
}

useRef Implementation

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();

  // Create ref object
  const ref = { current: initialValue };
  hook.memoizedState = ref;

  return ref;
}

function updateRef(initialValue) {
  const hook = updateWorkInProgressHook();
  // Just return the same ref object
  return hook.memoizedState;
}

useContext Implementation

// useContext doesn't use the hook linked list
function readContext(context) {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  // Track context dependency
  if (lastContextDependency === null) {
    const contextItem = {
      context,
      memoizedValue: value,
      next: null,
    };

    lastContextDependency = contextItem;
    currentlyRenderingFiber.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
    };
  } else {
    const contextItem = {
      context,
      memoizedValue: value,
      next: null,
    };
    lastContextDependency = lastContextDependency.next = contextItem;
  }

  return value;
}

Rules of Hooks

Why Hooks Must Be Called in Order

// This component has three hooks
function Counter() {
  const [count, setCount] = useState(0);      // Hook 1
  const [name, setName] = useState('React');   // Hook 2
  useEffect(() => { /* ... */ }, [count]);    // Hook 3

  // ...
}

// First render - hooks are mounted in order:
// Fiber.memoizedState:
//   Hook1(count) → Hook2(name) → Hook3(effect) → null

// On re-render, React walks the same list:
// Fiber.memoizedState:
//   Hook1(count) → Hook2(name) → Hook3(effect) → null
//     ↑              ↑              ↑
//   useState(0)  useState('React')  useEffect()

// If you conditionally call hooks:
function BrokenCounter({ condition }) {
  const [count, setCount] = useState(0);      // Hook 1

  if (condition) {
    const [name, setName] = useState('React'); // Hook 2 - SOMETIMES
  }

  useEffect(() => { /* ... */ }, [count]);    // Hook 2 or 3?

  // When condition changes from true to false:
  // React expects: Hook1 → Hook2 → Hook3
  // But gets:      Hook1 → Hook3 (useEffect)
  // Hook3 receives Hook2's state! BUG!
}

Visualization of Hook Order

┌─────────────────────────────────────────────────────────────────┐
│                    Hook Order Matters                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   CORRECT - Same order every render:                            │
│                                                                 │
│   Render 1:  useState(0) → useState('') → useEffect()          │
│   Render 2:  useState(0) → useState('') → useEffect()          │
│   Render 3:  useState(0) → useState('') → useEffect()          │
│                  ↓            ↓               ↓                 │
│              Hook 1       Hook 2          Hook 3                │
│                                                                 │
│   WRONG - Different order:                                      │
│                                                                 │
│   Render 1:  useState(0) → useState('') → useEffect()          │
│   Render 2:  useState(0) → useEffect()    ← SKIP!              │
│              Hook 1 gets  Hook 2 gets                          │
│              correct      WRONG STATE!                          │
│              state                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Custom Hooks

How Custom Hooks Work

// Custom hooks are just functions that call other hooks
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  const decrement = useCallback(() => {
    setCount(c => c - 1);
  }, []);

  return { count, increment, decrement };
}

// When used in a component:
function MyComponent() {
  const { count, increment } = useCounter(10);
  //       ↑
  // Under the hood, this creates 3 hooks:
  // 1. useState(10)
  // 2. useCallback(increment)
  // 3. useCallback(decrement)

  return <button onClick={increment}>{count}</button>;
}

Summary

In this chapter, you've learned:

  • Hook Storage: Linked list on fiber.memoizedState
  • Dispatcher Pattern: Different implementations for mount/update
  • useState: State stored in hook with update queue
  • useEffect: Effects stored separately with dependency tracking
  • useMemo/useCallback: Value caching with dependencies
  • Rules of Hooks: Why order must be consistent

Key Takeaways

  1. Linked list: Hooks are stored as a linked list
  2. Order matters: Hooks must be called in the same order
  3. Two phases: Mount creates hooks, update reuses them
  4. Eager bailout: useState can skip re-render if state unchanged
  5. Effect list: Effects are tracked separately for commit phase

Next Steps

Now that you understand hooks implementation, let's explore concurrent rendering features in Chapter 7: Concurrent Features.


Ready for Chapter 7? Concurrent Features

Generated for Awesome Code Docs

What Problem Does This Solve?

Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for hook, next, memoizedState so behavior stays predictable as complexity grows.

In practical terms, this chapter helps you avoid three common failures:

  • coupling core logic too tightly to one implementation path
  • missing the handoff boundaries between setup, execution, and validation
  • shipping changes without clear rollback or observability strategy

After working through this chapter, you should be able to reason about Chapter 6: Hooks Implementation as an operating subsystem inside React Fiber Internals, with explicit contracts for inputs, state transitions, and outputs.

Use the implementation notes around queue, deps, update as your checklist when adapting these patterns to your own repository.

How it Works Under the Hood

Under the hood, Chapter 6: Hooks Implementation usually follows a repeatable control path:

  1. Context bootstrap: initialize runtime config and prerequisites for hook.
  2. Input normalization: shape incoming data so next receives stable contracts.
  3. Core execution: run the main logic branch and propagate intermediate state through memoizedState.
  4. Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
  5. Output composition: return canonical result payloads for downstream consumers.
  6. Operational telemetry: emit logs/metrics needed for debugging and performance tuning.

When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.

Source Walkthrough

Use the following upstream sources to verify implementation details while reading this chapter:

  • Awesome Code Docs Why it matters: authoritative reference on Awesome Code Docs (github.com).

Suggested trace strategy:

  • search upstream code for hook and next to map concrete implementation paths
  • compare docs claims against actual runtime/config code before reusing patterns in production

Chapter Connections