© Bohua Xu

React Hooks Source Code Analysis

Dec 21, 2022 · 10min

Deep Dive into React Hooks: Source Code Analysis

Introduction

React Hooks, introduced in React 16.8, revolutionized how we manage state and side effects in functional components. This article delves deep into the implementation details of React Hooks, exploring how they work under the hood.

Core Concepts

React Hooks implementation is built on several fundamental concepts:

  1. Fiber Node: Each function component corresponds to a Fiber node where hooks state is stored
  2. Hook List: Multiple hooks in the same component are connected through a linked list
  3. Dispatcher: Responsible for switching different hook implementations in different phases (mount/update)
  4. WorkInProgress: Tracks the current hook being processed
  5. Memorization: Memory optimization mechanism for performance

Deep Dive into Hook Data Structures

Each Hook internally has the following data structure:

type Hook = {
  memoizedState: any,     // The state value stored by the Hook
  baseState: any,         // Base state value
  baseQueue: Update<any, any> | null,  // Base update queue
  queue: UpdateQueue<any, any> | null, // Update queue
  next: Hook | null,      // Pointer to the next Hook
};

type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,  // Pending updates
  interleaved: Update<S, A> | null, // Interleaved updates
  lanes: Lanes,           // Update priority
  dispatch: Dispatch<A> | null,  // Dispatch function
  lastRenderedReducer: ((s: S, a: A) => S) | null, // Last reducer used
  lastRenderedState: S | null,   // Last rendered state
};
type Hook = {
  memoizedState: any,     // The state value stored by the Hook
  baseState: any,         // Base state value
  baseQueue: Update<any, any> | null,  // Base update queue
  queue: UpdateQueue<any, any> | null, // Update queue
  next: Hook | null,      // Pointer to the next Hook
};

type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,  // Pending updates
  interleaved: Update<S, A> | null, // Interleaved updates
  lanes: Lanes,           // Update priority
  dispatch: Dispatch<A> | null,  // Dispatch function
  lastRenderedReducer: ((s: S, a: A) => S) | null, // Last reducer used
  lastRenderedState: S | null,   // Last rendered state
};

Hook Initialization Process

Here’s how hooks are created when a function component first renders:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Add hook to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Add hook to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

Complete useState Implementation

Let’s examine the detailed implementation of useState:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  
  // Handle initial state
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  
  hook.memoizedState = hook.baseState = initialState;
  
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  
  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch = (
    dispatchSetState.bind(null, currentlyRenderingFiber, queue),
  ));
  
  return [hook.memoizedState, dispatch];
}

// Update process
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}

// Actual state update handling
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const lane = requestUpdateLane(fiber);
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };

  // Fast path optimization: try to compute new state immediately
  if (fiber === currentlyRenderingFiber) {
    const eagerState = queue.lastRenderedReducer(queue.lastRenderedState, action);
    update.hasEagerState = true;
    update.eagerState = eagerState;
    if (Object.is(eagerState, queue.lastRenderedState)) {
      // State hasn't changed, no need to re-render
      return;
    }
  }

  // Add update to queue
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;

  scheduleUpdateOnFiber(fiber, lane);
}
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  
  // Handle initial state
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  
  hook.memoizedState = hook.baseState = initialState;
  
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  
  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch = (
    dispatchSetState.bind(null, currentlyRenderingFiber, queue),
  ));
  
  return [hook.memoizedState, dispatch];
}

// Update process
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}

// Actual state update handling
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const lane = requestUpdateLane(fiber);
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };

  // Fast path optimization: try to compute new state immediately
  if (fiber === currentlyRenderingFiber) {
    const eagerState = queue.lastRenderedReducer(queue.lastRenderedState, action);
    update.hasEagerState = true;
    update.eagerState = eagerState;
    if (Object.is(eagerState, queue.lastRenderedState)) {
      // State hasn't changed, no need to re-render
      return;
    }
  }

  // Add update to queue
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;

  scheduleUpdateOnFiber(fiber, lane);
}

Deep useEffect Implementation

The implementation of useEffect involves scheduling and cleanup of side effects:

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // Set effect flags
  currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
  
  hook.memoizedState = pushEffect(
    HookHasEffect | HookPassive,
    create,
    undefined,
    nextDeps,
  );
}

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  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 this effect
        hook.memoizedState = pushEffect(HookPassive, create, destroy, nextDeps);
        return;
      }
    }
  }

  // Dependencies changed, need to re-run effect
  currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
  hook.memoizedState = pushEffect(
    HookHasEffect | HookPassive,
    create,
    destroy,
    nextDeps,
  );
}
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // Set effect flags
  currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
  
  hook.memoizedState = pushEffect(
    HookHasEffect | HookPassive,
    create,
    undefined,
    nextDeps,
  );
}

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  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 this effect
        hook.memoizedState = pushEffect(HookPassive, create, destroy, nextDeps);
        return;
      }
    }
  }

  // Dependencies changed, need to re-run effect
  currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
  hook.memoizedState = pushEffect(
    HookHasEffect | HookPassive,
    create,
    destroy,
    nextDeps,
  );
}

Custom Hooks Working Mechanism

Custom Hooks are essentially compositions of primitive hooks, sharing the same Hook list:

function useCustomHook(initialValue) {
  // Create multiple Hooks in the same Fiber node's Hook list
  const [state, setState] = useState(initialValue);
  const prevValueRef = useRef(initialValue);
  const [derived, setDerived] = useState(null);

  useEffect(() => {
    // Effect creation will be added to the effect list
    if (state !== prevValueRef.current) {
      prevValueRef.current = state;
      setDerived(someComputation(state));
    }
  }, [state]);

  const customSetState = useCallback((newValue) => {
    // Callback hook also gets added to the hooks list
    setState(newValue);
    // Possible additional logic
  }, []);

  return [derived, customSetState];
}
function useCustomHook(initialValue) {
  // Create multiple Hooks in the same Fiber node's Hook list
  const [state, setState] = useState(initialValue);
  const prevValueRef = useRef(initialValue);
  const [derived, setDerived] = useState(null);

  useEffect(() => {
    // Effect creation will be added to the effect list
    if (state !== prevValueRef.current) {
      prevValueRef.current = state;
      setDerived(someComputation(state));
    }
  }, [state]);

  const customSetState = useCallback((newValue) => {
    // Callback hook also gets added to the hooks list
    setState(newValue);
    // Possible additional logic
  }, []);

  return [derived, customSetState];
}

Hook Scheduling and Priority

Hook updates are managed through React’s scheduling system:

function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }

  // Mark root has update
  markRootUpdated(root, lane);
  if (root === workInProgressRoot) {
    // Handle concurrent updates
    if ((workInProgressRootRenderLanes & lane) === NoLanes) {
      workInProgressRootUpdatedLanes |= lane;
    }
  }

  // Request schedule update
  ensureRootIsScheduled(root);
  
  if (lane === SyncLane) {
    // Synchronous updates execute immediately
    performSyncWorkOnRoot(root);
  }
}
function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }

  // Mark root has update
  markRootUpdated(root, lane);
  if (root === workInProgressRoot) {
    // Handle concurrent updates
    if ((workInProgressRootRenderLanes & lane) === NoLanes) {
      workInProgressRootUpdatedLanes |= lane;
    }
  }

  // Request schedule update
  ensureRootIsScheduled(root);
  
  if (lane === SyncLane) {
    // Synchronous updates execute immediately
    performSyncWorkOnRoot(root);
  }
}

Performance Optimization Mechanisms

React Hooks include multiple layers of performance optimization:

// 1. State batch updates
function batchedUpdates<A, R>(fn: A => R): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn();
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush synchronous queue
      flushSyncCallbackQueue();
    }
  }
}

// 2. Memoized computation results
function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// 1. State batch updates
function batchedUpdates<A, R>(fn: A => R): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn();
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush synchronous queue
      flushSyncCallbackQueue();
    }
  }
}

// 2. Memoized computation results
function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Error Handling Mechanisms

Hooks include comprehensive error handling:

function throwInvalidHookError() {
  throw new Error(
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
  );
}

function checkDepsAreArrayDev(deps: mixed) {
  if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
    console.error(
      'useEffect/useMemo/useCallback requires a dependencies array. ' +
        'Did you forget to pass an array?',
    );
  }
}
function throwInvalidHookError() {
  throw new Error(
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
  );
}

function checkDepsAreArrayDev(deps: mixed) {
  if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
    console.error(
      'useEffect/useMemo/useCallback requires a dependencies array. ' +
        'Did you forget to pass an array?',
    );
  }
}

Conclusion

The React Hooks implementation is a carefully designed system that achieves its goals through:

  1. Clever data structure design (Hook linked list)
  2. Excellent memory management (memoization)
  3. Efficient scheduling system
  4. Comprehensive error handling

This creates a state management solution that is both simple to use and powerful in capability. Understanding these implementation details not only helps us better use Hooks but also helps us more quickly identify and resolve issues when they arise.

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu