Let's Write React Hooks From Scratch

July 29, 2025

1,191 words

Post contents

In my article "The History of React Through Code", I talked a lot about how the rules of React Hooks were introduced so that it could interact correctly with the previous "Fiber" rewrite.

I'd like to think that I did a sufficient job explaining how this limitation led to more features in React and eventually even performance improvements through the React Compiler since React was able to store the state of a function relative to its parent component...

But wait, how does that, like, work?

An Array-Focused Intro to Hooks

After all, React doesn't transform a function component in any way, so how does useState persist its value internally?

Were we to try this without useState, we'd notice quickly how this behavior differs from a normal JavaScript function:

function Test() {	const a = 1;	console.log(a);	a++;}Test(); // 1Test(); // 1Test(); // 1

See, to make the magic of a function remembering state to work, Hooks don't just work alongside the VDOM, the method of persisting data in a component from a function requires the VDOM.

Here's one way we could persist state using a naïve implementation of hooks storage using an array:

const state = [];/** * React "increments" this internally * for each hook it runs into. * We won't for now, for simplicity. */let idx = 0;function useState(init) {	state[idx] = state[idx] ?? { val: init };	return [state[idx].val, (data) => (state[idx].val = data)];}function Test() {	const [data, setData] = useState(1);	console.log(data);	setData(data + 1);}Test(); // 1Test(); // 2Test(); // 3

While it may seem silly to use an array to store a Hook's state in a component, this is exactly how the React team teaches early insider knowledge about Hooks publicly:

Aside

It's because a Hook's state is stored in an array — or, in reality, a linked list — that explains why you can't conditionally call a hook, by the way.

If you were to conditionally call a hook, it would shift the index:

// First renderlet bool = true;function App() {	if (bool) useState("some"); // Idx 1	useState("val"); // Idx 2}// Second renderlet bool = false;function App() {	if (bool) useState("some"); // Skipped	useState("val"); // Idx 1 - recieves the "some" val from prior hook}

Component-focused state

Now that suffices as a high-level explaination, but let's expand this idea out a bit. After all, "React "increments" this internally for each hook it runs into."? That's a bit vague.

Let's try to implement this using the array storage system we came up with earlier, but instead lean into the VDOM's component tracking. This will allow us to store the array state in an abstract representation of the component via an internal Component class:

// Global reference to current componentlet currentComponent = null;// Component class to hold hook state arrayclass Component {	constructor() {		this.state = [];		this.currentHookIndex = 0;	}	render(renderFn) {		// Reset state for this render		currentComponent = this;		// Reset hook index for this render		this.currentHookIndex = 0;		// Call the component function		const result = renderFn();		return result;	}}function useState(init) {	const component = currentComponent;	const idx = component.currentHookIndex;	component.state[idx] = component.state[idx] ?? { val: init };	// Increment for next hook call	component.currentHookIndex++;	return [		component.state[idx].val,		(data) => (component.state[idx].val = data),	];}function Test() {	const [data, setData] = useState(1);	console.log(data);	setData(data + 1);}// Create component and run rendersconst component = new Component();component.render(Test); // 1component.render(Test); // 2component.render(Test); // 3

See, this internal Component class isn't just an idea I came up with; it's more representative of how state is stored in a VDOM node in React. When React decides it's time to render a given component, it pulls up the Hook state from the node.

A rework to linked lists

But wait, if we look at React's source code for a Hook's state:

export type Hook = {  memoizedState: any,  baseState: any,  baseQueue: Update<any, any> | null,  queue: any,  next: Hook | null,};

We'll see that there's stuff we'd expect, but then a reference to the next Hook in the chain...

Wait, Hooks are actually implemented internally using a linked list??

That's right!

In fact, we can even see that React stores a global reference of the current Hook being processed:

// Hooks are stored as a linked list on the fiber's memoizedState field. The// current hook list is the list that belongs to the current fiber. The// work-in-progress hook list is a new list that will be added to the// work-in-progress fiber.let currentHook: Hook | null = null;let workInProgressHook: Hook | null = null;

Knowing this, let's modify our implementation to track a component's Hook using a linked list:

// Linked list node for each hookclass Hook {  constructor() {    this.state = null;    this.next = null;  }}// Global state to simulate React's internalslet currentComponent = null;let workInProgressHook = null;// Component class to hold hook linked listclass Component {  constructor() {    this.hooks = null; // Head of the linked list  }  render(renderFn) {    // Set up for this component's render    currentComponent = this;    workInProgressHook = this.hooks;        // Call the component function    const result = renderFn();        // Clean up    currentComponent = null;    workInProgressHook = null;        return result;  }}// useState implementation using linked listfunction useState(initialState) {  let hook;    if (workInProgressHook === null) {    // Create new hook and add to linked list    hook = new Hook();    hook.state = initialState;        // Find end of list and append, or set as first hook    if (currentComponent.hooks === null) {      currentComponent.hooks = hook;    } else {      let lastHook = currentComponent.hooks;      while (lastHook.next !== null) {        lastHook = lastHook.next;      }      lastHook.next = hook;    }  } else {    // Use existing hook from linked list    hook = workInProgressHook;  }    // Move to next hook for subsequent useState calls  workInProgressHook = hook.next;    const setState = (newState) => {    hook.state = newState;  };    return [hook.state, setState];}function MyComponent() {  const [count, setCount] = useState(1);  console.log(count);  setCount(count + 1);}const component = new Component();component.render(MyComponent); // 1component.render(MyComponent); // 2component.render(MyComponent); // 3

Conclusion

Hopefully this has been helpful to explore the internals of React Hooks and their storage in a function.

Want to now learn how to leverage React's Hooks effectively in your applications and even how to write your own version of React from scratch?

Well, I've written a free book series teaching React, Angular, and Vue all at once that goes from newcomer at the start to framework author by the end of it. Take a look:

The Framework Field Guide

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.