How React Hooks Work: Fiber Linked Lists and the useState Update Mechanism

useState is not just syntactic sugar in React. At its core, it is a Hook node in a Fiber-linked list organized strictly by call order. This design solves state persistence and update scheduling for function components. Keywords: React Hooks, useState, Fiber.

The technical snapshot clarifies the runtime model

Parameter Details
Language JavaScript / TypeScript
Core runtime React Fiber
State protocol Dispatcher + Hook Linked List + UpdateQueue
Article focus useState internals, Hook order constraints, scheduling mechanism
GitHub stars Not provided in the source material
Core dependencies React, Fiber, Scheduler concepts

Hooks are fundamentally an ordered linked list, not ordinary function calls

React does not store useState state inside the function itself. Instead, it attaches state to the Fiber node associated with the current function component. The Fiber field memoizedState points to the head of the entire Hook linked list.

type Fiber = {
  memoizedState: Hook | null; // Points to the head of the current component's Hook linked list
};

This definition shows that every time a function component runs, React reads or rebuilds the Hook linked list around the Fiber.

The Dispatcher determines whether a Hook takes the mount, update, or error path

During the render phase, React first sets the dispatcher. The initial render uses the mount logic, later renders use the update logic, and invalid Hook calls go down the error path.

const HooksDispatcherOnMount = {
  useState: mountState, // Create the Hook during the mount phase
  useEffect: mountEffect,
};

const HooksDispatcherOnUpdate = {
  useState: updateState, // Reuse the Hook during the update phase
  useEffect: updateEffect,
};

const ContextOnlyDispatcher = {
  useState: throwInvalidHookError, // Throw immediately on invalid calls
};

This set of dispatchers is the entry point for Hook runtime dispatching and the direct reason Hooks can only be called at the top level of a component.

The Hook data structure carries state and the update queue

A single Hook is not very complex, but it must store the current value, the base state, the update queue, and a reference to the next Hook.

type Hook = {
  memoizedState: any; // The memoized value for the current Hook
  baseState: any; // The base state used for update calculation
  queue: UpdateQueue | null; // The update queue for the current Hook
  next: Hook | null; // Points to the next Hook
};

The easiest part to confuse here is the two different memoizedState fields. Fiber.memoizedState stores the first Hook, while Hook.memoizedState stores the actual data for that specific Hook.

Fiber.memoizedState
  ↓
[useState] → [useEffect] → [useMemo] → null

This chain depends entirely on call order for positioning, which is why you cannot place Hooks inside conditions, loops, or nested functions.

useState creates the Hook and dispatch function during the mount phase

When rendering starts, React clears the Hook state on the current workInProgress Fiber, then decides whether to use the mount or update dispatcher based on whether the component already has an old Fiber.

function renderWithHooks(current, workInProgress, Component, props) {
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null; // Clear the old Hook linked list reference
  workInProgress.updateQueue = null; // Clear the effect queue

  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  const children = Component(props); // Execute the component and call Hooks in order
  ReactCurrentDispatcher.current = ContextOnlyDispatcher; // Disallow arbitrary Hook usage after cleanup
  return children;
}

This flow shows that Hooks are not declared ahead of time. React registers them in order while the component function executes.

mountState appends the Hook to the current Fiber linked list

During mount, every call to useState creates a new Hook node and appends it to the end of the linked list. At the same time, React creates the queue and the dispatch function.

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

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook; // Use the first Hook as the head node
  } else {
    workInProgressHook = workInProgressHook.next = hook; // Append to the end of the linked list
  }

  return workInProgressHook;
}

function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState = initialState; // Initialize the state value

  const queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer, // The built-in reducer used by useState
  };

  hook.queue = queue;
  queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
  return [hook.memoizedState, queue.dispatch];
}

This shows that useState is essentially a simplified form of useReducer with a built-in reducer.

useState reuses old Hooks during updates instead of creating new ones

Once React enters the update phase, it no longer creates Hooks blindly. Instead, it reuses them one by one along the old Fiber Hook chain. This reuse has only one precondition: the Hook call order in the current render must exactly match the previous one.

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState); // useState reuses the useReducer update logic
}

If you place a Hook inside a conditional branch, the Hook order in the first render and the second render can shift. React may then treat a position that originally belonged to useState as if it belonged to useRef or useEffect.

function App({ showNumber }) {
  if (showNumber) {
    React.useState(0); // Conditional calls break Hook order
  }
  React.useState(1);
  React.useRef(null);
}

The root problem in this pattern is not the conditional itself. It is the fact that the linked-list slots become misaligned.

Diagram of Hook order misalignment AI Visual Insight: This diagram shows how Hook order maps onto the Fiber Hook linked list. Each Hook occupies a fixed slot, and React matches state during updates by position, not by variable name. If one Hook is skipped conditionally, all subsequent nodes shift forward, causing state, refs, and effects to be reused in the wrong places.

setState fundamentally enqueues an update and triggers scheduling

When you call setState, React does not immediately change the value. Instead, it creates an update object, pushes it into the current Hook’s update queue, and then schedules the Fiber to render again.

function dispatchSetState(fiber, queue, action) {
  const update = { action, next: null }; // Create a single state update
  enqueueUpdate(queue, update); // Put it into the current Hook queue
  scheduleUpdateOnFiber(fiber); // Trigger scheduling
}

The key point in this logic is asynchronous enqueueing. That is why the behavior of multiple consecutive setState calls ultimately depends on queue merging and scheduling timing.

The UpdateQueue recalculates the latest state in order

You can think of the useState queue as a sequence of pending updates. During render, React iterates through these updates and folds them into the new state one by one with the reducer.

function processUpdateQueue(baseState, queue, reducer) {
  let state = baseState;
  queue.forEach((update) => {
    state = reducer(state, update.action); // Consume updates in order
  });
  return state;
}

That is why functional updates like setCount(c => c + 1) are more reliable: they continue from the previously computed result directly.

The Hook scheduling mechanism depends on Fiber and the priority system working together

After React enqueues an update, it uses scheduleUpdateOnFiber to mark the current Fiber with priority, then enters the render and commit phases.

setState
  ↓
enqueue update
  ↓
scheduleUpdateOnFiber
  ↓
render (interruptible)
  ↓
commit (non-interruptible)

This design allows React to repeat rendering in concurrent scenarios while keeping Hook behavior stable, because every render walks the entire Hook chain from the beginning in the same order.

Hook linked list and update misalignment diagram AI Visual Insight: This diagram emphasizes Hook reuse during updates. React clones or reuses nodes one by one according to the Hook order on the old Fiber. If the new render contains more Hooks, fewer Hooks, or a different order, the reuse pointers no longer align, eventually triggering errors such as “Rendered more hooks than during the previous render.”

The best way to understand useState is to see it as a state machine interface

From the source-code perspective, useState is not just a variable container. It is a state interface jointly exposed by Fiber, the Hook linked list, the UpdateQueue, and the scheduler.

Once you understand this model, three common conclusions become clear: why Hooks must be called at the top level, why useState resembles useReducer, and why setState does not take effect synchronously.

The FAQ answers the most common questions

1. Why must React Hooks be called in a fixed order?

Because React locates state by a Hook’s position in the linked list, not by variable name. Once the order changes, old state can be incorrectly reused by a different Hook.

2. What is the relationship between useState and useReducer?

useState is essentially a special case of useReducer. Internally, it uses basicStateReducer, so it shares the same update queue and scheduling model while exposing a simpler API.

3. Why might the value still be old right after calling setState?

Because setState creates an update and enqueues it. React computes the actual next state later during a subsequent render rather than writing it back immediately in the current call stack.

Core Summary: This article explains the essence of useState from the React Fiber perspective: Hooks are mounted onto Fiber as an ordered linked list and rely on call order for state lookup. It also breaks down the mount, update, dispatch, and scheduling flow to show why conditionally calling Hooks causes errors.