React gives us something called hooks that let us use features like state inside function components. One of the most popular hooks is useState
. But have you ever wondered how it works behind the scenes?
In this blog, we’ll build our own simple version of useState
called useCustomState
. This will help you understand the main idea behind how React stores and updates values in function components.
Step 1 : A Very Basic useState
Let’s start by implementing a very basic version of useState
:
let stateValue;
const useCustomState = (initialValue) => {
if (stateValue === undefined) {
stateValue = initialValue;
}
const setValue = (newValue) => {
stateValue = newValue;
renderApp(); // Re-render the app with new state
};
return [stateValue, setValue];
};
This custom hook keeps the state in a global variable stateValue
. Here's how we use it :
const App = () => {
const [count, setCount] = useCustomState(0);
return <button onClick={()=> setCount(count + 1)}>Counter : {count}</button>;
};
The renderApp()
function re-renders the component :
const renderApp = () => {
ReactDOM.render(<App />, document.getElementById('root'));
};
What's Happening?
- On the first render,
stateValue
isundefined
, so it's initialized to0
. - Clicking the button updates the
stateValue
and re-renders the app. - On re-render, the hook returns the updated
stateValue
.
The Limitation
This works well - until we try to use more than one useCustomState
in the same component. Let’s say :
const [countA, setCountA] = useCustomState(0);
const [countB, setCountB] = useCustomState(0);
since we only have one stateValue
, both hooks overwrite each other. We need a better system.
Step 2 : Managing Multiple States
To handle multiple stateful hooks, we can use an array to track values :
let stateValues = [];
let callIndex = 0;
const useCustomState = (initialValue) => {
const currentIndex = callIndex;
if (stateValues[currentIndex] === undefined) {
stateValues[currentIndex] = initialValue;
}
const setValue = (newValue) => {
stateValues[currentIndex] = newValue;
renderApp(); // trigger re-render
};
callIndex++;
return [stateValues[currentIndex], setValue];
};
And inside our component :
const App = () => {
callIndex = 0; // reset before each render
const [countA, setCountA] = useCustomState(0);
const [countB, setCountB] = useCustomState(0);
return (
<>
<button onClick={()=> setCountA(countA + 1)}>
Counter A : {countA}
</button>
<button onClick={()=> setCountB(countB + 1)}>
Counter B : {countB}
</button>
</>
);
};
Why reset callIndex
?
On every render, hooks are called in the same order. This order is critical - it's how React matches the correct state to the correct hook.
What's the Takeaway?
This is a simplified version of React's hook system. Here's what we learn :
- Hooks rely on call order to maintain consistent state.
- Each hook invocation gets its own slot in a shared state array.
- React uses this pattern internally (in a much more robust way) to power
useState
,useEffect
, and more.
Conclusion
While we should never use this in production, this exercise is an awesome way to build intuition for how React handles hooks under the hood. Understanding this can help debug tricky issues, and deepen our appreciation for the React design choices.