Understanding React useState Hook by Building It from Scratch

April 12, 2025

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 is undefined, so it's initialized to 0.
  • 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>
      &nbsp;&nbsp;
      <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.