Implementing React useState Hook from Scratch: Detailed Explanation and Code
This article explains how to recreate the React useState hook by following the React source execution flow, describing the update mechanism, circular linked‑list queue, fiber storage, scheduling simulation, and differences from the official implementation, all illustrated with complete JavaScript code examples.
The article introduces a minimal implementation of React's useState hook that mirrors the real React source behavior, allowing readers to understand the inner workings of hooks by studying a concise, functional demo.
Working principle
When a component renders, a useState call creates or retrieves a hook object stored in the component's fiber. Updates are represented by update objects linked in a circular singly‑linked list inside the hook's queue . Each click on a p element generates a new update, which is then processed during the next render.
Update data structure
const update = {
action, // the function that computes the new state
next: null // link to the next update (circular list)
};The first update forms a self‑referencing loop ( update.next = update ) and is stored as queue.pending . Subsequent updates are inserted before queue.pending , keeping the list circular.
Hook data structure
const hook = {
queue: { pending: null },
memoizedState: initialState,
next: null
};Each useState call gets its own hook; the hooks are linked together in a plain singly‑linked list on the fiber ( fiber.memoizedState ).
Dispatching an update
function dispatchAction(queue, action) {
const update = { action, next: null };
if (queue.pending === null) {
update.next = update; // single update forms a circular list
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
schedule(); // simulate React's scheduling step
}The schedule function resets the workInProgressHook pointer to the first hook of the fiber and triggers a re‑render, after which isMount becomes false for subsequent updates.
Processing updates during render
function useState(initialState) {
let hook;
if (isMount) {
// create a new hook on the first render
hook = { queue: { pending: null }, memoizedState: initialState, next: null };
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
// retrieve the existing hook on updates
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
// compute the new state from pending updates
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}The component example demonstrates mounting and updating:
function App() {
const [num, updateNum] = useState(0);
console.log(`${isMount ? 'mount' : 'update'} num: `, num);
return {
click() { updateNum(n => n + 1); },
focus() { updateNum(n => n + 3); }
};
}Calling window.app.click() in the online demo simulates a user click, causing the state to increment. Multiple useState calls are also supported, each maintaining its own hook chain.
Differences from real React Hooks
Real React uses a dispatcher instead of a simple isMount flag.
React can skip low‑priority updates (batchedUpdates) whereas this implementation processes every update.
React assigns priorities to updates, enabling more sophisticated scheduling.
The article concludes with a link to a free open‑source e‑book for deeper exploration of the React source code.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.