React useState internal structure: secrets of functional updates and linked lists
Implementing useState directly in vanilla JS, we dig into why functional updates are necessary and why React chose linked lists.
4 min readreact
Published
Oct 03, 2025
Reading time
4 min
Sections
10
On this page10+-
Why does React update the screen automatically?
If I were to create a counter in vanilla JavaScript, I would write it like this.
let count = 0;
const addButton = document.getElementById('button');
const countSpan = document.getElementById('count');
addButton.addEventListener('click', () => {
count++;
// You need to update the DOM yourself
countSpan.textContent = count;
});
The screen does not change with just count++. You must explicitly call countSpan.textContent = count.
Just call setCount and the screen will update automatically. There is no code anywhere that directly manipulates the DOM.
How is this possible?
In this article, we will try to implement useState ourselves and answer two key questions:
Why does it only increase by 1 even if setCount(count + 1) is called multiple times? (Need for functional update)
Why does React use linked lists instead of arrays? (Component Isolation)
Implement useState yourself
If you want to implement useState in JavaScript, there is one thing you need to answer first.
Where should the state be stored?
In fact, component functions are executed each time they are rendered, so how is the state maintained?
function Counter() {
let count = 0;
const increment = () => {
count++;
render();
};
}
For example, if written as above, count is initialized to 0 every time the function is executed.
This means that state cannot be placed inside a function.
So what if we put the state outside of the function?
const MyReact = (function () {
let state; // Store outside of function
function useState(initialValue) {
if (state === undefined) {
state = initialValue;
}
const setState = (newValue) => {
state = newValue;
render();
};
return [state, setState];
}
// ...render function
})();
The state is now maintained. But a new problem arises.
What happens if you call useState multiple times in one component? As in the code above, if there is only one state variable, all states will be overwritten.
If so, you need a way to distinguish each useState, and there are probably two ways to implement it.
Create variables for each state => (state1, state2, state3...) => The problem is that the number of states is not known in advance.
Collect and store the states somewhere => Save it as an array and find it by index
In case 1, this may not be a problem in small apps, but as the number of components and hooks increases, too many variables are used.
Therefore, I used arrangement number 2.
const MyReact = (function () {
let hooks = []; // Store states in an array
let currentIndex = 0; // What is the current useState?
function useState(initialValue) {
const hookIndex = currentIndex; // Capture with Closure
if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = initialValue;
}
const setState = (newValue) => {
hooks[hookIndex] = newValue;
render();
};
currentIndex++; // For the next useState
return [hooks[hookIndex], setState];
}
function render() {
currentIndex = 0; // Initialize each render
// ...
}
})();
First useState(0) call → stored in hooks[0], currentIndex is set to 1
Second useState('React') call → saved in hooks[1], currentIndex is set to 2
Third useState(20) call → Save in hooks[2]
Why functional updates are needed
Now, if you test the demo, you'll notice that there's a bit of a problem.
At the time the incrementThree function is executed, count is fixed to 0. (Closure)
This means that each setCount call triggers render(), but does not change the count variable inside the incrementThree function that is already executing.
The official React documentation says that the solution to this problem is to update the state as shown below.
But why does it refer to the latest value when passing a function?
When passing a value, count + 1 is calculated immediately at function execution time.
However, if you pass a function, prev => prev + 1 is executed inside setState and receives the latest value at that point.
Looking at the code implemented above,
const setState = (newValue) => {
// Check if it is a function
if (typeof newValue === 'function') {
hooks[hookIndex] = newValue(hooks[hookIndex]); // Pass latest value
} else {
hooks[hookIndex] = newValue;
}
render();
};
In fact, React’s internal code also follows the same pattern.
// ReactFiberHooks.js
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
// action is newValue based on our code
// state is hooks[hookIndex] based on our code
Why a linked list rather than an array?
The current implementation seems to be working well, but it has some critical issues that make it difficult to use in production.
In other words, even if the child is unmounted, only the corresponding fiber and hook chain need to be removed from memory. Other components are not affected at all.
Cleanup
I implement useState myself and have noticed two important design decisions:
Functional update
Problem: Due to closure, the previous value is referenced even if called multiple times.
Solved: Use the latest value at the time of execution by passing a function
// X setCount(count + 1)
// O setCount(prev => prev + 1)
Linked List
Problem: Global arrays cause interference between components
Solved: Each component has its own independent hook chain.
If you want to explore further
Among React source code,
packages/react-reconciler/src/ReactFiberHooks.js
basicStateReducer - Functional update logic
updateWorkInProgressHook - Linked list traversal
mountState vs updateState - step by step