How Hooks Are Stored Internally
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
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:
- Checks if this is a mount (first render) or update (re-render)
- On mount: creates a new hook object, appends it to the linked list, initializes state
- On update: advances to the next hook in the existing linked list, reads stored state, processes any pending updates
- 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.
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>;
}
| What developers do | What 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 |
- 1Hooks are stored as a linked list on the Fiber node — matched by call order, not by name
- 2Never call hooks conditionally, in loops, or in nested functions — it changes the list order
- 3Mount creates new hook nodes; update reads existing nodes by advancing a pointer
- 4The 'use' prefix on custom hooks is a signal to the linter to enforce Rules of Hooks
- 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.