Implementing the useState Hook

Introduction

I felt uneasy the first time I read about hooks in React. Their inner workings seemed too magical. I remember looking at a simple example and trying to make sense of how it worked under the hood:

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
        The count is: {count}
        <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

It was clear what the example was doing. You click the + button, and the count is incremented. But where was the value of count being stored, and how were we getting the correct value, even though 0 was passed every time? Even as I started incorporating hooks into my apps, I had few clear answers. So I started searching for sources that described how hooks work under the hood. Finally, I decided to try and reimplement some of the core hooks myself.

This post details my process of reimplementing the useState hook. For me, the goal was never to exactly match the real implementation. The goal was to gain some insight into how some like useState can be implemented.

Classes and state

Generally speaking, state includes any value that changes over time, when that value needs to be remembered by the program. For React class components, the concept of state is translated directly into the state object. The idea is to encapsulate all (or at least most) of the changing values in one place. We initialized the state object with some default values when the class is created, and then modify these values indirectly by calling the setState method:

class Counter extends React.Component {
    constructor() {
      this.state = {
        count: 0
      }
    }

    increment = () => {
      this.setState({
        count: this.state.count + 1
      }) 
    }

    render() {
      return (
        <>
          <div>count: {this.state.count}</div>
          <button onClick={this.increment}>+</button>
        </>
      )
    }
}

The setState method recreates the component's state by merging the existing state with the new object that was passed as an argument. If we were to implement the base setState, it would look something like this:

  setState(newPartialState) {
    this.state = {
      ...this.state,
      ...newPartialState
    }
    // rerender the component
  }

Functions and State

Unlike an object or class, a function cannot internally maintain state. This is the reason, in React, that a functional component is also called a stateless functional component. So I'd come to expect a functional component to work the same way as a simple add function - given the same input, I would expect to always get the same output. If I needed state, I would have to create a parent class component, and have that component pass down the state:

// The Counter functional component will receive 
// the count and a setCount function 
// from a parent class component
const Counter = ({ count, setCount }) => (
  <>
    <div>count: {count}</div>
    <button onClick={() => setCount(count + 1)}>+</button>
  </>
)

class CounterContainer extends React.Component {
  // shorthand for using a constructor
  state = {
    count: 0
  }

  setCount = (newCount) => {
    this.setState({
      count: newCount
    }) 
  }

  render() {
    return (
      <Counter count={this.state.count} setCount={this.setCount}>
    )
  }
}

In a sense, the useState hook gives us a way to tell React that we need something like that parent class component, without having to create it ourselves. We simply tell React that we want to use state, and React will create that state for us.

Functions that use state

As a first attempt around creating a parent class component, we could try and have a function component directly modify a global variable:

let count = 0;

const Counter = () => (
  <>
    <div>{count}</div>
    <button onClick={() => count++}>+</button>
  </>
)

This, however, doesn't quite work. Even though value of count is changing, the Counter component does not re-render to show the new value. We stil need something similar to a setState call, which would rerender the component when the value of count changes. We can make a setCount function that does just that:

let count = 0

function setCount(newCount) {
  count = newCount
  ReactDOM.render(<Counter />)
}

const Counter = () => (
  <>
    <div>{count}</div>
    <button onClick={() => setCount(count++)}>+</button>
  </>
)

This works! To ensure count and setCount are always used together, we can put them inside an object. Let's call this object MyReact:

const MyReact = {
  count: 0,
  setCount(newCount) {
    this.count = newCount;
    ReactDOM.render(<Counter />)
  }
}

To make things even clearer, let's create a useCount function that returns an object with count and setCount:

  useCount() {
    return { 
      count: this.count,
      setCount: this.setCount
    }
  }

Next, we would want to allow the caller of useCount to pass an initial value. This presented us with a problem. we only need to set the initial value on the very first time that useCount is called. On any subsequent call, we would want use the existing value of useCount. One solution is adding a stateInitialized variable. We'll initially set it to false, and set it to true on the first time that useCount is called:

  stateInitialized: false,
  useCount(initialValue) {
    if (!this.stateInitialized) {
      this.count = initialValue;
      this.stateInitialized = true;
    }
    // ...
  }

Now that we got the basics working, we can make MyReact more general by renaming the count variable to state, and the method names to useState and setState. Also, we'll return state and setState in an array, to allow for easy renaming:

const MyReact = {
  state: null,
  stateInitialized: false,
  setState(newState) {
    this.state = newState;
    ReactDOM.render(<Counter/>, rootElement);
  },
  useState(initialValue) {
    if (!this.stateInitialized) {
      this.stateInitialized = true;
      this.state = initialValue;
    }
    return [this.state, this.setState];
  }
};

const Counter = () => {
  const [count, setCount] = MyReact.useState(0)
  // ...
}

We can also added a render method to MyReact, and call this method instead of calling ReactDOM.render. This will allow us to save the Counter component as part of MyReact:

  // ...
  setState(newState) {
    this.state = newState;
    ReactDOM.render(<this.component/>, this.rootElement);
  },
  // ...
  render(component, rootElement) {
    this.component = component;
    this.rootElement = rootElement;
    ReactDOM.render(<this.component/>, this.rootElement);
  }
  // ..

// later 
MyReact.render(Counter)

Multiple state variables

The next step is to enable MyReact to manage multiple variables. The first step is to replace the single state variable with an array of state variables. Now we would need some way to know, each time setState was being called, which state variable is the one that needs to change. We can achieve this by relying on the call order to useState. Take, for example, the two subsequent calls below:

const MyCounter = () => {
  const [count, setCount] = MyReact.useState(0);
  const [name, setName] = MyReact.useState("");
}

The MyReact.useState methods would always be executed in the same order, first returning the values of count1, setCount1, and then returning the values of name, setName. This will be the case as as long as MyReact.useState is not called inside conditional block, where the condition isn't always true or false.

Now, since we have two or more state variables, each state variable will need to have a corresponding setState method. We can achieve this by using an array of objects, where object stores the state value and the corresponding setState method. We can call each of the objects a statePair and the arrays that holds them stateArray.

[{ value: count, setCount }, { value: name, setName }, ...]

We now need a way to track which element of the array is being used at any given time. For example, having the two calls to MyReact.useState above, the first call should return the [count, setCount] and the second call should return [name, setName]. We can use a variable to track this value. Let's call this variable currentStateIndex.

The currentStateIndex will be reset to 0 whenever any setState is called. When the value of currentStateIndex becomes equal to the length of the array, we will create a new pair of state an setState.

const MyReact = {
  stateArr: [],
  currentStateIndex: 0,
  component: null,
  useState(initialValue) {
    // if we reached beyond the last element of the array
    // We will need create a new state
    if (this.currentStateIndex === this.stateArr.length) {
      const statePair = {
        value: initialValue,
        setState(newValue) {
          statePair.value = newValue;
          MyReact.currentStateIndex = 0;
          ReactDOM.render(<MyReact.component />, rootElement);
        }
      };

      this.stateArr.push(statePair);
    }
    // get the current state and setState before incrementing the index
    const currentStatePair = this.stateArr[this.currentStateIndex];
    this.currentStateIndex += 1;
    return [currentStatePair.value, currentStatePair.setState];
  },
  render(component, rootElement) {
    this.component = component;
    this.rootElement = rootElement;
    ReactDOM.render(<this.component />, this.rootElement);
  }
};

Example

Given the above implementation, let's try and follow an example of a component that uses two state variables:

const Counter = () => {
  const [count1, setCount1] = MyReact.useState(0);
  const [count2, setCount2] = MyReact.useState(0);
  return (
    <>
      <div>
        The first count is: {count1}
        <button onClick={() => setCount1(count1 + 1)}>+</button>
      </div>
      <div>
        The second count is: {count2}
        <button onClick={() => setCount2(count2 + 1)}>+</button>
      </div>
    </>
  )
}

MyReact.render(Counter)

Below is a sandbox with MyReact and the Counter component:

Following the example, these would be the inital values of MyReact:

MyReact {  
  stateArr: [],
  currentStateIndex: 0,
  component: null,
}

After the first call to useState:

const Counter = () => {
  const [count1, setCount1] = MyReact.useState(0); // <--

The values of MyReact will be:

MyReact {  
  stateArr: [{ value: 0, setState: fn() }],
  currentStateIndex: 1,
  component: Counter,
}

After the second call to useState:

const Counter = () => {
  const [count1, setCount1] = MyReact.useState(0); 
  const [count2, setCount2] = MyReact.useState(0); // <--

The values of MyReact will be:

MyReact {  
  stateArr: [{ value: 0, setState: fn() }, { value: 0, setState: fn() }],
  currentStateIndex: 2,
  component: Counter,
}

Now, if the first + button is pressed, the values of MyReact would become:

MyReact {  
  stateArr: [{ value: 1, setState: fn() }, { value: 0, setState: fn() }],
  currentStateIndex: 0,
  component: Counter,
}

Which would lead to Counter being rendered again. On the subsequent calls to useState, only the currentStateIndex will be incremented, while the existing elements of stateArr will be returned.

Conclusion

So, we've arrived at something pretty similar to React's useState hook. I cannot say if understanding the internal workings of hooks would make someone a better React developer. But I do feel that is is worthwhile to try and understand how abstractions can be created - this can help us understand the ones that have already been made, and to make new abstractions of our own.