Building React.useState From Scratch

In React, hooks are just functions that let you preserve state between renders. One of the most widely used hooks is useState.
When you call useState, it gives you two things:
The current value of the state
A function to update that state
The key is that React remembers the state between renders. To better understand how this works, we’ll build our own simplified version of useState.
The Simplest Possible useState
A very basic implementation could look like this:
function useState(initState) {
let _val = initState; // The internal state
let state = () => _val; // Getter
let setState = (newVal) => {
_val = newVal; // Setter
};
return [state, setState];
}
const [state, setState] = useState(0);
console.log(state());
setState(3);
console.log(state());
Every time you call the
stategetter function, you get the latest value.When you call
setState, it updates that value inside a closure.
This works, but it has three major problems:
It only works for one piece of state
It doesn’t persist across multiple renders of a component. Moreover, we don’t have any Component at this point but we will be building one in this article
Calling
state()as a function (or its setter) is not howuseStatenormally works in React
Our To-Do
In React, a component can have multiple useState calls. To mimic this, our implementation needs to:
Remember which state belongs to which
useStatecallKeep track of the order in which they’re called
Persist them across renders
React does this internally using an array of state values and a pointer to track which one is currently being accessed.
Introducing a React Namespace
To organize things, we’ll wrap our hooks logic inside an IIFE (Immediately Invoked Function Expression).
const React = (() => {
function useState(initState) {
// ... Our Original Logic Goes Here
}
return {
useState,
};
})();
This creates a React namespace that clearly separates the implementation from its usage — similar to how the real React library is structured.
For testing, we’ll define a minimal Component function.
const Component = () => {
const [count, setCount] = React.useState(3);
return {
render: () => {
console.log(count);
},
onClick: () => {
setCount((prev) => prev + 1);
},
};
};
It returns an object with two functions:
render: Logs the current value of state. This simulates a UI render so you can see values change in real time.onClick: Updates the state using the setter function, mimicking a UI event like a button click.
Improving Persistence
Our first improvement is moving _val outside of the useState function, making it a persistent variable. With this change, the getter and setter behave more like React’s useState.
const React = (() => {
let _val;
function useState(initState) {
let state = _val || initState;
let setState = (newVal) => {
if (typeof newVal === 'function') {
_val = newVal(state);
} else _val = newVal;
};
return [state, setState];
}
return {
useState,
};
})();
Inside setState, we add this crucial logic:
If the new value is a function (e.g.,
setCount(prev => prev + 1)), we call it with the current state value and store the result.If the new value is a direct value (e.g.,
setText("Hello World")), we store it directly.
This ensures that our setter matches React’s behavior, handling both direct value updates and functional updates that depend on the previous state.
Simulating Re-Renders
Next, we introduce a render function inside the React namespace. This function executes the component and returns it, letting us simulate re-renders — just like React does when state changes.
// ...Inside React namespace
const render = (Comp) => {
let C = Comp();
C.render();
return C;
};
///....Values Returned from the Namespace
return {
render,
useState,
}
However, our logic still only works for one variable. If we add multiple useState calls, the code breaks.
Supporting Multiple States
To fix this, we:
Rename
_valtohooksand make it an arrayAdd an
indexvariable to track whichuseStatecall is currently being processed
This ensures that each useState within a component gets its own independent slot in the hooks array.
Final Code
const React = (() => {
let hooks = [];
let index = 0;
function useState(initialValue) {
const currentIndex = index;
hooks[currentIndex] = hooks[currentIndex] ?? initialValue;
const setState = (newValue) => {
if (typeof newValue === "function") {
hooks[currentIndex] = newValue(hooks[currentIndex]);
} else {
hooks[currentIndex] = newValue;
}
};
index++;
return [hooks[currentIndex], setState];
}
function render(Component) {
index = 0;
const c = Component();
c.render();
return c;
}
return { useState, render };
})();
Usage Example
const Component = () => {
const [count, setCount] = React.useState(3);
const [text, setText] = React.useState("Hello");
return {
render: () => {
console.log(text);
console.log(count);
},
onClick: () => {
setCount((prev) => prev + 1);
setText("HELLO WORLD");
},
};
};
let renderer = React.render(Component);
renderer.onClick();
renderer = React.render(Component);
Step-by-Step Execution and Explanation
First Render
The
hooksarray is empty.useState(3)setscount = 3and stores it inhooksat index 0.useState("Hello")setstext = "Hello"and stores it inhooksat index 1.
Render Output:
Hello
3
Handling Updates
renderer.onClick()triggers two updates:setCount(prev => prev + 1)updateshooksfrom3→4.setText("HELLO WORLD")updateshooksfrom"Hello"→"HELLO WORLD".
Second Render
When
React.render(Component)runs again,indexresets so eachuseStatematches the correct slot inhooks.countnow reads4.textnow reads"HELLO WORLD".
Render Output:
HELLO WORLD
4
Conclusion
By re-creating useState from scratch, we’ve peeled back the curtain on one of React’s most powerful features. Instead of relying on magic, we can now see that React’s state management boils down to a simple idea: store values in an array and track them by their call order.
This exercise demonstrates how React:
Persists values across re-renders
Handles multiple independent pieces of state
Supports both direct updates and functional updates based on the previous state
While this is only a minimal simulation, the core principle is the same in React itself — just at a much larger scale with added optimizations, scheduling, and performance guarantees. Thank you for being patient with me.



