Avatar

Fixing React's useState

08 Nov 2020

React’s useState has a big problem. Its name is wrong. The proper name for useState should be getOrCreateState because that’s what it actually does.

For this post, let’s imagine that useState is called getOrCreateState, and try to see how it would work

Overview

For classic class components, the state was managed by the component object itself.

For hooks though, the state has been moved inside of React itself.

Image

Now, to get the value from inside of React you call the getOrCreateState

const value = getOrCreateState(defaultValue);

This moves the burden of maintaining state from your code to React, which can be seen as a blessing or a curse depending on who you ask.

How react stores component state

Internally React keeps a simple array state for storing all component state.

// Inside React's source code

let state = [];

All the state values that you use are stored inside of this simple state array.

Getting and Creating values

React also has a currentIndex, used for fetching data from its state array.

Each call to getOrCreateState will get the value at currentIndex (i.e. state[currentIndex]) and increment it.

If the array runs out of values (i.e. currentIndex is greater than the length of state), it will create a new entry using the defaultValue that you pass in and push it into the state array.

Essentially

// Inside React's source code

let state = [], currentIndex = 0;

function getOrCreateState(defaultValue) {
  if (currentIndex >= state.length) {
    state.push(defaultValue);
  }

  let value = state[currentIndex];
  currentIndex++;
  return value;
}

Note: Each time React does a re-render, the currentIndex is reset to 0. This means that the first render creates the values and each subsequent render gets the value from the state array. That’s how React state is persisted between renders.

Updating values

So now you can get and create values from inside the state array, but what if you want to update a value?

One way would be to return the a key a.k.a. the currentIndex and use that to update the state

function getOrCreateState(defaultValue) {
  if (currentIndex >= state.length) {
    state.push(defaultValue);
  }

  let value = state[React.currentIndex];
  let key = currentIndex;

  currentIndex++;

  return [value, key];
}

function updateState(key, value) {
  state[key] = value;
}

and inside your component

let [value, key] = getOrCreateState(defaultValue)
....
updateState(key, newValue);
...

But there are a few reasons that the React team is against exposing the key: https://overreacted.io/why-do-hooks-rely-on-call-order/

Assuming that we want to do the same and not expose the key, how do we allow updating the value in that case?

Well, we know that,

  1. The key is needed mostly for the updateState function
  2. We already know the key inside of the getOrCreateState function

So one thing we can do is create the update function inside the getOrCreateState function (where the key is already known) and just return the update function itself

function getOrCreateState(defaultValue) {
  if (currentIndex >= state.length) {
    state.push(defaultValue);
  }

  let value = state[currentIndex];
  let updateFn = (newValue) => state[currentIndex] = newValue;

  currentIndex++;

  return [value, updateFn];
}

and we can use it like

let [value, updateValue] = getOrCreateState(defaultValue)
....
updateValue(newValue);
...

Now we no longer need to expose the key, but we can still update the value

If you noticed already we’ve basically implemented useState hook from scratch. A lot of times hooks feel like magic, but internally it’s just plain javascript arrays and functions

Problem: re-fetching updated values

Now for the problem you mentioned,

You can see from the implementation that updateValue updates the value inside the array. Essentially similar to this

// what `useState` returns
const value = state[i];

// what `setState` does
state[i] = newValue;

// At this point `state[i]` has the `newValue`,
// but the `value` variable still holds the old value

console.log(value) // Old value
console.log(state[i]) // New value

Variables that hold the old value won’t be automatically updated, since the old value is left untouched after doing a setState, only the state array is changed. So after doing an update (i.e. setState), you need to call useState again to get the updated value.

This is the reason you kept seeing the old value even after you called setState with the new value.

Solution

Since you can’t access React’s internal state and you can’t get values by their index either, the only proper way to see the updated value is when React does a re-render, i.e. when the component function is called again and useState is called inside of it.

There are hacks around this, and you could also use useRef, but that’s going to cause more unexpected bugs, and may actually cause other parts of the app to start failing.

Conclusion

React’s useState is a lot simpler than it initially looks. One caveat is that React actually uses a linked list to store for state, not an array like I described in the post. Both would work, but React has a concurrent mode planned, and arrays would be a bad fit for that in the future.