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
state
getter 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 howuseState
normally 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
useState
callKeep 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
_val
tohooks
and make it an arrayAdd an
index
variable to track whichuseState
call 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
hooks
array is empty.useState(3)
setscount = 3
and stores it inhooks
at index 0.useState("Hello")
setstext = "Hello"
and stores it inhooks
at index 1.
Render Output:
Hello
3
Handling Updates
renderer.onClick()
triggers two updates:setCount(prev => prev + 1)
updateshooks
from3
→4
.setText("HELLO WORLD")
updateshooks
from"Hello"
→"HELLO WORLD"
.
Second Render
When
React.render(Component)
runs again,index
resets so eachuseState
matches the correct slot inhooks
.count
now reads4
.text
now 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.
Subscribe to my newsletter
Read articles from Shoeb Ilyas directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
