DEV Community

Cover image for Build Your Own React useState Hook Under 40 Lines
Nikhil Dabhade
Nikhil Dabhade

Posted on

Build Your Own React useState Hook Under 40 Lines

useState looks simple on the surface, but behind it is one of the core ideas that makes React work: persistent state + re-rendering. In this article, we'll build a tiny version of useState ourselves to understand the mental model behind it.

Understanding the Basic Idea Behind useState

First, we know that the useState returns a state and a function to update that state.

const [state, setState] = useState()
Enter fullscreen mode Exit fullscreen mode

Think of this as a function that returns a value and a setter function which updates the value.

function useState() {
    let state;

    const setState = (newState) => {
      state = newState;
    };

    return [state, setState];
 }
Enter fullscreen mode Exit fullscreen mode

That's it! Quite simple isn't it.

Not really, we are missing the initial value and one issue which you might notice if you look carefully (Take a moment to find the issue).

Why Our State Keeps Resetting

The issue is that we have declared the state inside useState, So on every render the useState will get called and the state will become undefined every time.

First render -> value was undefined -> you made it to 1 -> Second Render -> value again became undefined.

After fixing that issue, we finally have something closer to what we wanted.

Persisting State Between Renders

let state;
function useState<T>(initialValue:T) {
    if(state === undefined){
      state = initialValue
    }
    const setState = (newState:T) => {
      state = newState;
    };

    return [state, setState];
 }
Enter fullscreen mode Exit fullscreen mode

Nice! we have built our own useState hook and now it's time to use it and check the results of our hard work. I've used our hook inside that App.tsx and added one handleIncrement to increase the count

useState hook implementation

Why Updating State Still Doesn't Work

Wait a second, even after clicking on the increment button the number is not updating. But why's that!

Because React has no idea the state changed. When React's real useState updates, React schedules the component to render again.

So we'll need to tell the react to render the component if our state changes.

Making React Re-render

That's where the useSyncExternalStore react hook comes into the picture. This hook tells the react to check for our state updates, re-render the component and give us the updated state.

Understanding useSyncExternalStore

const value = useSyncExternalStore(
  subscribe,
  getSnapshot
);
Enter fullscreen mode Exit fullscreen mode

React expects two functions here:

  • subscribe → tells React how to listen for changes
  • getSnapshot → tells React how to read the latest value

That’s it.

Unlike useState, React does not manage the state for you here.

Instead, React simply acts as a subscriber to an external store.

Understanding subscribe

When React calls your subscribe function, it gives you a listener callback.

const subscribe = (listener: () => void) => {
  listeners.add(listener);

  return () => listeners.delete(listener);
};
Enter fullscreen mode Exit fullscreen mode

That listener is the function React wants you to call whenever your store changes.

Later, when state updates, we manually trigger all listeners:

listeners.forEach((listener) => listener());
Enter fullscreen mode Exit fullscreen mode

And once those listeners run, React knows it should re-render subscribed components.

Final Implementation

// useState.ts
import { useSyncExternalStore } from "react";

const listeners = new Set<() => void>();
let state;

function useState<T>(initialState: T) {
  if (state === undefined) {
    state = initialState;
  }
  const setState = (newState: T) => {
    if (state === newState) return;
    state = newState;
    listeners.forEach((listener) => listener());
  };

  const getState = () => state;

  const subscribe = (listener) => {
    listeners.add(listener);

    return () => listeners.delete(listener);
  };

  const updatedState = useSyncExternalStore(subscribe, getState);

  return [updatedState, setState];
}

export { useState };
Enter fullscreen mode Exit fullscreen mode

We have stored the listeners in a set and called them only when the state changes (if previous value is 1 and new value is also 1 then just return, no re-renders).

What happens after you click the handleIncrement

setState()
   ↓
update state
   ↓
notify listeners
   ↓
React re-renders
   ↓
new state appears
Enter fullscreen mode Exit fullscreen mode

Now you'll see that our custom hook is finally working!

final working useState hook implementation

Where Our Implementation Falls Short

So is this how the React useState works? ummm not really, i mean we have a good mental model of how it might be but the actual react useState does a lot more than this.

  1. Our version doesn't support multiple state, even if you create more than one state using our hook it will just override the previous state.

  2. React supports functional updates setState(prev => prev + 1);, something you can build on top of this as practise.

  3. Our state compare in setState (line no. 11) works for primitive types, but what about objects? find out!

And there are many more, this implementation was just to give you the idea of how useState and useSyncExternalStore works.

Final Thoughts

We didn’t recreate React’s real useState implementation here.

Internally, React does something much more using the Fiber architecture and hook tracking.

But building a tiny version like this helps make the “magic” behind hooks feel much more understandable.

And honestly, that’s one of the best ways to learn React internals:
build simplified versions yourself.

Top comments (0)