React Hooks don’t actually use a linked list internally. They use an array, and the "linked list" analogy is a useful mental model for understanding how React keeps track of hook state between renders, but it’s not the literal data structure.

Here’s how it looks in practice. Imagine a component Counter that uses useState twice:

function Counter() {
  const [count1, setCount1] = React.useState(0);
  const [count2, setCount2] = React.useState(10);

  return (
    <div>
      <p>Count 1: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment 1</button>
      <p>Count 2: {count2}</p>
      <button onClick={() => setCount2(count2 + 1)}>Increment 2</button>
    </div>
  );
}

When React renders Counter for the first time, it creates a special "memoized" version of the component. Attached to this memoized component is an array, let’s call it hooks. For Counter, this array would look something like this after the first render:

hooks = [ { memoizedState: 0, queue: [], tag: 0 }, { memoizedState: 10, queue: [], tag: 0 } ]

Each object in the hooks array represents a single hook call. memoizedState holds the current state value. queue is where pending state updates are stored. tag is an internal identifier for the hook type (0 is useState).

Now, let’s say you click "Increment 1". React needs to update the state for the first useState call. It finds the corresponding hook object in the hooks array by its index. Since useState(0) was the first hook, it’s at index 0.

The update is added to the queue of the hook at index 0:

hooks[0].queue.push({ next: null, payload: 1, priority: null })

When React re-renders Counter, it iterates through the hooks again. For the hook at index 0, it sees there’s a pending update in its queue. It processes this update, changes memoizedState to 1, and clears the queue. The hooks array now looks like:

hooks = [ { memoizedState: 1, queue: [], tag: 0 }, { memoizedState: 10, queue: [], tag: 0 } ]

If you were to click "Increment 2" before the re-render from "Increment 1" completes, the update would be added to hooks[1].queue. React would process both updates during the re-render.

The "linked list" idea comes from how React would have to manage this if the order of hooks changed. If you had a conditional hook call, like:

function ConditionalHook({ showExtra }) {
  const [value, setValue] = React.useState(5);
  if (showExtra) {
    const [extra, setExtra] = React.useState('hello');
    // ... use extra
  }
  // ... use value
}

If showExtra is true on the first render, the hooks array might be [{ memoizedState: 5, ... }, { memoizedState: 'hello', ... }]. If showExtra becomes false on the next render, the second hook is skipped. React would then see that the hook at index 1 is missing, and it would bail out of processing further hooks for that render. This is why the rules of hooks exist: the order must be consistent. If React were using a true linked list, it could potentially skip nodes more flexibly, but its array-based implementation relies on stable indices.

The core problem React solves here is state persistence across renders without relying on instance properties like in class components. Each render of a function component creates a new execution context. Without a mechanism to store state outside that context, the state would be lost. React attaches this hooks array to the component’s fiber node (an internal data structure representing the component in React’s reconciliation tree), ensuring that the state is preserved between renders and is accessible via the correct hook by its position in the array.

The queue itself is often implemented as a linked list internally for efficient addition and processing of updates. When multiple updates are batched, they can be linked together, allowing React to iterate through them without needing to reallocate memory for a new array each time. This is where the "linked list" part does come into play, but it’s for the updates within a hook, not for the hooks themselves.

The most surprising thing about React’s hook implementation is how it leverages the fiber architecture to associate this mutable array of hook states with a component instance. Each component instance (represented by a fiber node) gets its own hooks array. When a component re-renders, React traverses the fiber tree and, for each function component, it accesses its fiber node’s memoizedState (which points to the head of the hook list/array) to retrieve and update hook states. This is a deeply integrated system where the reconciliation algorithm and the hook state management are tightly coupled.

The queue for a hook, like useState, isn’t just a simple array. It’s a circular doubly linked list. When setCount is called, a new object containing the update (payload) is created and added to the end of this list. During the next render, React iterates through this queue, applying each update to memoizedState and then clearing the queue. This circular structure allows for efficient insertion and removal of update objects.

The next concept you’ll likely run into is how useEffect and other side-effect hooks manage their cleanup functions, and how React ensures they are executed at the correct time relative to renders and unmounts.

Want structured learning?

Take the full React course →