How does useState work under the hood? What happens when you call it multiple times in one component?
`useState` relies on React’s internal hook call order and fiber node linkage—each call appends a new `memoizedState` and `updateQueue` to the current fiber’s `memoizedState` linked list, with strict call-order guarantees enforced by React’s dispatcher.
Short Answer
React implements useState by associating each call with a sequentially allocated slot in the component’s fiber node (fiber.memoizedState), forming a singly linked list of hook objects—so multiple useState calls create distinct, order-dependent state entries tied to the same fiber.
Details
When a functional component renders, React’s reconciler sets currentlyRenderingFiber = workInProgressFiber and initializes renderLanes and dispatcher. Each useState(initialState) call invokes mountState() (on first render) or updateState() (on re-renders), which:
- Reads from
currentlyRenderingFiber.memoizedState(a linked list ofHookobjects); - For mount: creates a new
HookwithmemoizedState = lazyInit ? lazyInit(initialState) : initialState,baseState = memoizedState,queue = { pending: null, dispatch: boundDispatch }, and links it to the fiber’s hook chain; - For update: walks the hook chain using
nextCurrentHook(derived fromcurrentFiber.alternate?.memoizedState) and applies queued updates viaprocessUpdateQueue; - The
dispatchfunction (a closure overqueueandlane) schedules a new render withenqueueUpdate(fiber, update, lane)and triggers reconciliation.
Crucially, React does not use closures or variable names—it uses call order to map eachuseState()to its correspondingHooknode. Violating the Rules of Hooks (e.g., calling conditionally) breaks this index alignment, causing state corruption.
Example
function Counter() {
const [count, setCount] = useState(0); // Hook #0 → fiber.memoizedState
const [name, setName] = useState(''); // Hook #1 → fiber.memoizedState.next
const [isOn, toggle] = useState(false); // Hook #2 → fiber.memoizedState.next.next
// ...
}Internally, fiber.memoizedState points to a Hook node containing count’s state and queue; its .next points to the name hook; .next.next points to isOn—all statically ordered at render time.
Bonus
To stand out: explain how concurrent rendering affects this—e.g., during an interleaved render, useState may read from currentFiber.alternate.memoizedState and apply updates from both pending queues (current + work-in-progress). Also mention that ReactCurrentDispatcher.current switches between HooksDispatcherOnMount, HooksDispatcherOnUpdate, and HooksDispatcherOnRerender—enabling different behavior for initial mount vs. update vs. bailout. Bonus insight: useState is not magic—it’s just a thin wrapper around useReducer({ type: 'init' }, initialState), revealing React’s unification of state logic under the reducer abstraction.