Intro
Ever wonder how useState works under the hood? Especially how a call to setState can trigger a UI rendering? Let's take a peek into a simple implementation of useState.
The simple implementation is taken from step 8 of "Build your own React" tutorial by Rodrigo Pombo.
Annex on the Fiber tree
Understanding the Fiber tree data structure and the two Fiber trees used in React lifecycles, current and wip, are important to understand the code of useState. I wrote extensively about the Fiber tree here and the two Fiber trees as part of the React lifecycle here.
Here are a few important notes.
Fiber tree is a tree data structure, where each node represents either a DOM node (h1, div, p, etc.) or a functional component. The entire Fiber tree can be thought of as a representation of the virtual DOM.
The current tree reflects the DOM that is currently visible on the browser. When state is changed and a new render is triggered, the wip tree can be thought of as a draft of the new DOM that is about to be rendered on the browser. Changes are made to this wip throughout a render (thus, the name work-in-progress).
There are two properties of a Fiber node that we care about.
- The
alternatefield is a pointer to the corresponding node in the previous render. Okay, what does "corresponding" mean here? - The
hooksfield points to an array of hooks associated with that node (so only nodes that represent functional components can have non-emptyhooks)
useState under the hood
function useState(initial: any) {
// Initiate the hook for the current rendering
const oldHook =
wipFiber?.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
// apply all the actions saved from last rendering
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action: Function) => {
hook.state = action(hook.state);
});
function setState(action: Function) {
// all the actions are pushed into the queue
hook.queue.push(action);
// set wipRoot to initiate the rendering
wipRoot = {
type: currentRoot.type,
props: currentRoot?.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
}
return [hook.state, setState];
}
Initiate the hook
In the first render, the state of the hook will be initiated with initial value. In subsequent renders, the state of the hook will be initiated as the state of the corresponding hook in the previous render.
While the first sentence is straightforward, the second is a bit hard to see from the code. Let's zoom in...
const oldHook =
wipFiber?.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
From the wipFiber tree, wipFiber.alternate accesses the corresponding fiber node in the current tree , and wipFiber.alternate.hooks access the hooks of that node.
The index of a hook in a functional component doesn't change through renders, we can use a global variable hookIndex to track and retrieve the hook corresponding to the current hook in the previous render.
Defining setState
Now, let's look at the setState function. When setState is called with a callback function action, the action is added to a queue that is part of the hook. This queue will be unpacked in the next render, and each actions will be applied on the state.
When setState is called, it triggers a render by setting the root of the wip tree from null to the root Fiber node. However, the render is not triggered immediately. There is another function that, whenever the browser runs out of high-priority tasks, will check if wip is null. If wip is not null, it will actually begin re-rendering.
Apply all the actions on state
Functional component are essentially functions that will be run in each render. Every time a functional component is run, all of its useState will be triggered, which means new hooks and new setState functions will be created. In addition...
// apply all the actions saved from last rendering
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action: Function) => {
hook.state = action(hook.state);
});
All the actions that were saved in the oldHook.queue in the previous render (remember in the setState of the previous render where we push the action into the hook.queue?) will be applied on the old state to retrieve the new state.
Final words
And that, folks, is the anatomy of the useState function. If you want to see how useState fits into the bigger picture, definitely check out Rodrigo's tutorial.
Top comments (0)