So React hooks have been released for a while now and they are great! I have used them in production code and it makes everything look nicer. As I continued to use hooks, I started to wonder how all this magic works.
Apparently I was not the only one because there was a Boston React meetup on this topic. Big Thank you to Ryan Florence and Michael Jackson (Not the Moonwalking legend) for giving such a great talk around the subject. Continue watching and you will learn more about useEffect
and how that works!
How does it work?
You create a functional component and throw some React hook at it that tracks state, can also update it, and it just works.
Many of us have seen some variation of this example before:
One useState
import React from "react";
const App = () => {
const [count, setCount] = React.useState(1);
return (
<div className="App">
<h1>The infamous counter example</h1>
<button onClick={() => setCount(count - 1)}>-</button>
<span style={{ margin: "0 16px" }}>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};
export default App;
Ok great, but how does it do that magic? Look at the React.useState
line. It's so easy to read that I never questioned it. I have a destructed array that extracts the count
value and some function called setCount
and it will initialize count with the default value that I passed into useState
. What happens when I add another React.useState
to the picture?
Two useState
, ha-ha-ha
Count Dracula anyone?
const App = () => {
const [count, setCount] = React.useState(1);
const [message, setMessage] = React.useState("");
const adder = () => {
if (count < 10) {
setCount(count + 1);
setMessage(null);
} else {
setMessage("You can't go higher than 10");
}
}
const subtracter = () => {
if (count > 1) {
setCount(count - 1);
setMessage(null);
} else {
setMessage("You can't go lower than 1, you crazy");
}
}
return (
<div className="App">
<h1>The infamous counter example</h1>
<button onClick={subtracter}>-</button>
<span style={{ margin: "0 16px" }}>{count}</span>
<button onClick={adder}>+</button>
<p>{message}</p>
</div>
);
};
Now we are showing a message whenever a user is trying to go outside the bounds of 1 - 10
In our component, we have two destructured arrays that are using the same React.useState
hook with different default values. Whoa, now we are getting into the magic of it all.
Alright so lets delete our React
from React.useState
we should get a referenceError saying, "useState is not defined"
Let's implement our own useState
function.
Reverse engineering the useState
function
A useState
function has a value and a function that will set that value
Something like this:
const useState = (value) => {
const state = [value, setValue]
return state
}
We are still getting referenceErrors because we haven't defined setValue
. We know that setValue is a function because of how we use it in useState
Our count useState
: const [count, setCount] = React.useState(1);
Calling setCount
: setCount(count + 1);
Creating the setValue
function results in no more error but the -
and +
buttons don't work.
const useState = (value) => {
const setValue = () => {
// What do we do in here?
}
const state = [value, setValue]
return state
}
If we try and change the default value in useState
it will update count
ππ½. At least something is working π.
Moving on to figuring out what the hell setValue
does.
When we look at setCount
it's doing some sort of value reassignment and then it causes React to rerender. So that is what we are going to do next.
const setValue = () => {
// What do we do in here?
// Do some assigning
// Rerender React
}
We will pass in a new value argument to our setValue
function.
const setValue = (newValue) => {
// What do we do in here?
// Do some assigning
// Rerender React
}
But what do we do with newValue
within the setValue
function?
const setValue = (newValue) => {
// Do some assigning
value = newValue // Does this work?
// Rerender React
}
value = newValue
makes sense but that does not update the value of the counter. Why? When I console.log
within setValue
and outside of setValue
this is what we see.
So After I refresh the page. The count is initialized to 1 and the message is initialized to null, great start. I click the +
button and we see the count value increase to 2, but it does not update count on the screen. π€ Maybe I need to manually re-render the browser to update the count?
Implement a janky way to manually re-render the browser
const useState = (value) => {
const setValue = (newValue) => {
value = newValue;
manualRerender();
};
const state = [value, setValue];
return state;
};
.
.
.
const manualRerender = () => {
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
};
manualRerender();
This still doesn't update count in the browser. What the heck?
I was stuck on this for a little while and now I know why. Let's console.log
state right after we create it.
const state = [value, setValue];
console.log(state)
Our call to useState
causes the first render, and we get:
[1, setValue()]
And on our second call to useState
we render:
[null, setValue()]
To help visualize this a bit better, let's add a render tracker to count how many times we render the screen.
let render = -1
const useState = (value) => {
const setValue = (newValue) => {
value = newValue;
manualRerender();
};
const state = [value, setValue];
console.log(++render)
console.log(state)
return state;
};
How does our setValue
function know which value to update? It doesn't, therefore we need a way to track it. You can use an array or an object to do this. I choose the red pill of objects.
Outside of useState
function, we are going to create an object called states
const states = {}
Within the useState
function initialize the states
object. Let's use the bracket notation to assign the key/value pair.
states[++render] = state
I am also going to create another variable called id
that will store the render value so we can take out the ++render
within the brackets.
You should have something that looks like this:
let render = -1;
const states = {};
const useState = (value) => {
const id = ++render;
const setValue = (newValue) => {
value = newValue;
manualRerender();
};
const state = [value, setValue];
states[id] = state;
console.log(states);
return state;
};
What does our states
object look like?
states = {
0: [1, setValue],
1: [null, setValue]
}
So now when we click the add and subtract buttons we get... nothing again. Oh right because value = newValue
still isn't doing anything.
But there is something that is happening. If you look at the console you will see that every time we click on one of the buttons it will keep adding the same arrays to our states
object but count
isn't incrementing and message is still null.
So setValue
needs to go look for value
, then assign the newValue
to value
.
const setValue = (newValue) => {
states[id][0] = newValue;
manualRerender();
};
Then we want to make sure we are only updating keys: 0 and 1 since those will be our two useState
locations.
So head down to the manualRerender
function and add a call to render
and reassign it to -1
const manualRerender = () => {
render = -1;
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
};
We do this because every time we call setValue it will call the manualRerender
function setting render
back to -1
Lastly, we will add a check to see if the object exists. If it does then we will just return the object.
if (states[id]) return states[id];
Phew. That was a lot to process and this is just a very simplistic approach to useState
. There is a ton more that happens behind the scenes, but at least we have a rough idea of how it works and we demystified it a bit.
Take a look at all the code and try and make a mental model of how it all works.
Hope this helps π
Top comments (3)
This reminds me of ASP.net Viewstate. It was a structure that paralleled the structure of the UI control tree - there were no identifiers. Values were loaded into controls by matching the two structures. If you messed around with it then you could push values into the wrong control.
Thank you, appreciate your feedback :D.