Skip to content

How Hooks Are Stored Internally

intermediate12 min read

Hooks Are Not Magic — They Are a Linked List

Ever wonder how React knows which useState is which? It doesn't use the variable name. It doesn't use any identifier. Every hook call in your component maps to a node in a linked list stored on the component's Fiber node, matched purely by call order. This is the single most important implementation detail that explains every rule of hooks.

function MyComponent() {
  const [name, setName] = useState('Alice');   // Hook 1
  const [age, setAge] = useState(30);           // Hook 2
  const ref = useRef(null);                      // Hook 3
  useEffect(() => { /* ... */ }, []);            // Hook 4

  return <div>{name}, {age}</div>;
}

Internally, React stores this as:

Fiber.memoizedState → Hook1{state:'Alice'} → Hook2{state:30} → Hook3{ref:{current:null}} → Hook4{effect:...} → null
Mental Model

Think of hooks as a numbered checklist. React goes down the list in order: item 1 is your first useState, item 2 is your second useState, item 3 is your useRef. On every render, React walks the same checklist in the same order and matches each hook call to its stored data by position. If you skip item 2 on one render (conditional hook), item 3's data gets matched to item 2's slot — everything is wrong.

The Fiber Node

Every component instance has a corresponding Fiber node in React's internal tree. The Fiber stores:

// Simplified Fiber node structure
{
  tag: FunctionComponent,       // Component type
  type: MyComponent,            // The function itself
  memoizedState: hook1,         // HEAD of hooks linked list
  memoizedProps: { /* ... */ },
  stateNode: null,              // DOM node (for host components)
  // ... scheduling, effects, etc.
}

memoizedState points to the first hook. Each hook has a next pointer to the following hook.

The Hook Object

Each hook in the linked list has this structure:

// Simplified hook object
{
  memoizedState: value,    // The stored value (state, ref, memo result, effect)
  baseState: value,        // Base state before pending updates
  baseQueue: null,         // Pending updates from previous render
  queue: {                 // Update queue for setState
    pending: null,
    dispatch: setStateFn,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: value,
  },
  next: nextHook,          // Pointer to next hook in the list
}
How React processes hooks on render

React maintains a global variable called currentlyRenderingFiber that tracks which component is being rendered. When you call useState(), React:

  1. Checks if this is a mount (first render) or update (re-render)
  2. On mount: creates a new hook object, appends it to the linked list, initializes state
  3. On update: advances to the next hook in the existing linked list, reads stored state, processes any pending updates
  4. Returns [currentState, dispatch]

The key insight: React knows which hook to use purely by position in the list. There is no magic — just a pointer advancing through nodes.

Why the Rules of Hooks Exist

Now you can see why those "rules of hooks" aren't arbitrary bureaucracy — they're structural requirements of a linked list.

Rule 1: Only call hooks at the top level

// BROKEN — conditional hook
function BrokenComponent({ isLoggedIn }) {
  if (isLoggedIn) {
    const [name, setName] = useState(''); // Hook 1 (sometimes)
  }
  const [age, setAge] = useState(0);       // Hook 1 or Hook 2?
  useEffect(() => { /* ... */ }, []);       // Hook 2 or Hook 3?

  // When isLoggedIn changes from true to false:
  // React expects: Hook1=useState, Hook2=useState, Hook3=useEffect
  // React gets:    Hook1=useState(age), Hook2=useEffect
  // Age reads name's stored state. Effect reads age's stored data. CORRUPTION.
}

React does not know which hook is which. It walks the linked list in order. If the order changes between renders, hooks read the wrong stored data.

// CORRECT — hooks always called in the same order
function CorrectComponent({ isLoggedIn }) {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  useEffect(() => { /* ... */ }, []);

  // Conditionals go INSIDE the hook, or use the value conditionally
  // Not around the hook call itself
}

Rule 2: Only call hooks from React functions

// BROKEN — hook in a regular function
function getFormattedName(name) {
  const [formatted, setFormatted] = useState(name); // No Fiber context!
  return formatted;
}

// CORRECT — hook in a custom hook (prefixed with "use")
function useFormattedName(name) {
  const [formatted, setFormatted] = useState(name);
  return formatted;
}

Hooks need the currentlyRenderingFiber context. Regular functions do not have it. The use prefix is not just a convention — it tells the linter to enforce rules of hooks inside that function.

Common Trap

The Rules of Hooks are not arbitrary conventions — they are structural requirements of the linked list storage model. Breaking them does not always cause an immediate error. Sometimes the corruption is silent — wrong state values, effects with wrong dependencies, refs pointing to wrong elements. The bugs surface later as mysterious behavior that is nearly impossible to trace.

Mount vs Update: Two Different Dispatchers

Here's something most people don't realize: React uses two completely different sets of hook implementations depending on the render phase:

// Simplified — what React does internally
const HooksDispatcherOnMount = {
  useState: mountState,      // Creates hook, initializes state
  useEffect: mountEffect,    // Creates hook, schedules effect
  useRef: mountRef,          // Creates hook, creates ref object
};

const HooksDispatcherOnUpdate = {
  useState: updateState,     // Reads existing hook, processes queue
  useEffect: updateEffect,   // Reads existing hook, compares deps
  useRef: updateRef,         // Reads existing hook, returns same ref
};

On mount, each hook call creates a new node in the linked list. On update, React advances a workInProgressHook pointer through the existing list.

Production Scenario: Debugging Hook Order Issues

// This bug is subtle and hard to catch
function UserDashboard({ features }) {
  const [name, setName] = useState('');

  // BUG: hooks called inside a loop with variable iteration count
  const featureStates = features.map(feature =>
    useState(feature.defaultValue)  // Number of hooks changes if features changes!
  );

  useEffect(() => {
    console.log('Dashboard mounted');
  }, []);

  return <div>{name}</div>;
}

The fix: move dynamic hook calls into separate components or use a single state object:

function UserDashboard({ features }) {
  const [name, setName] = useState('');
  const [featureValues, setFeatureValues] = useState(
    () => Object.fromEntries(features.map(f => [f.id, f.defaultValue]))
  );

  useEffect(() => {
    console.log('Dashboard mounted');
  }, []);

  return <div>{name}</div>;
}
Execution Trace
Mount render
currentlyRenderingFiber = MyComponent's fiber
React prepares to render the component
useState('Alice')
Create Hook1 { memoizedState: 'Alice', next: null }
First hook — becomes head of linked list
useState(30)
Create Hook2 { memoizedState: 30 }, Hook1.next = Hook2
Second hook — linked after Hook1
useRef(null)
Create Hook3 { memoizedState: `{current: null}` }, Hook2.next = Hook3
Third hook — linked after Hook2
Update render
workInProgressHook = Hook1
Re-render — start at head of existing list
useState('Alice')
Read Hook1.memoizedState, advance to Hook2
Returns stored state, advances pointer
useState(30)
Read Hook2.memoizedState, advance to Hook3
Returns stored state, advances pointer
What developers doWhat they should do
Calling hooks conditionally: if (show) { useState() }
Conditional hooks change the linked list order between renders. Hook N reads Hook N-1's data. State corruption.
Always call hooks unconditionally. Use the result conditionally.
Calling hooks in loops: items.map(() => useState(...))
If the array length changes, the number of hooks changes. Same corruption as conditional hooks.
Use a single state object, or render each item as a separate component with its own hooks
Calling hooks in regular functions (not use-prefixed)
The use prefix signals to the linter that Rules of Hooks apply. Regular functions have no Fiber context.
Create custom hooks with the 'use' prefix: useMyHook()
Calling hooks in event handlers: onClick={() => { useState() }}
Event handlers are called outside React's render cycle. There is no currentlyRenderingFiber context.
Call hooks at the top level of the component or custom hook
Quiz
How does React know which useState call gets which stored state?
Quiz
What happens internally when a conditional hook is skipped on re-render?
Quiz
What is stored in Fiber.memoizedState for a function component?
Key Rules
  1. 1Hooks are stored as a linked list on the Fiber node — matched by call order, not by name
  2. 2Never call hooks conditionally, in loops, or in nested functions — it changes the list order
  3. 3Mount creates new hook nodes; update reads existing nodes by advancing a pointer
  4. 4The 'use' prefix on custom hooks is a signal to the linter to enforce Rules of Hooks
  5. 5Hook order corruption may be silent — wrong state values, not always an error

Challenge: Predict the Corruption

Challenge: Hook Order Bug Analysis

// What state values do name and count have after the
// second render when showGreeting changes from true to false?

function BuggyComponent({ showGreeting }) {
  if (showGreeting) {
    const [greeting, setGreeting] = useState('Hello');
    // On first render: Hook 1 = 'Hello'
  }

  const [name, setName] = useState('Alice');
  // On first render: Hook 2 = 'Alice'

  const [count, setCount] = useState(0);
  // On first render: Hook 3 = 0

  return <div>{name} - {count}</div>;
}

// Render 1: showGreeting = true
// Render 2: showGreeting = false
Show Answer

On Render 2, the linked list still has three hooks from Render 1:

  • Hook 1: memoizedState = 'Hello' (was greeting)
  • Hook 2: memoizedState = 'Alice' (was name)
  • Hook 3: memoizedState = 0 (was count)

But now the component only calls two hooks:

  • useState('Alice') reads Hook 1 → gets 'Hello' (WRONG — reads greeting's state)
  • useState(0) reads Hook 2 → gets 'Alice' (WRONG — reads name's state)

Result: name = 'Hello', count = 'Alice'

Both variables have the wrong values. The name displays the old greeting value, and count holds a string instead of a number. In practice, React would likely throw a "Rendered fewer hooks than expected" error in development mode, but the corruption illustrates why conditional hooks are forbidden.

1/11