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.
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,
- The
key
is needed mostly for theupdateState
function - We already know the
key
inside of thegetOrCreateState
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.