Introduction
Nowadays, we are still using the useState hook to set a variable in a React component. The useState, is introduced as 'hooks', is written like this
const [count, setCount] = React.useState<number>(0);
tsx
But what really is this? Why do we have to use this hook just to set a variable that holds a number and get incremented?
Why don't we just use something like this?
let count = 0;
count++;
tsx
Well, it always works in our first counter app with Vanilla JavaScript. Why don't we use it on React then?
TLDR;
React does a re-render by calling the component function, and with every function calls, your variable will get reset every single time.
Stepping Back
Before we jump into the React Core Concept, let's step back to Vanilla JavaScript. For this demo, we are going to build a simple counter app.
let count = 0;
function add() {
count++;
document.getElementById('count').textContent = count;
}
jsx
Simple right? When the button—that has add()
as a click listener—triggers, we add the count and update the text by accessing the documents.
If we look closely, we can see that it is doing 3 actions. Let's break it down into its own functions.
// Declare
let count = 0;
function mutate() {
count++;
}
function render() {
document.getElementById("count").textContent = count;
}
// event listener pseudocode
when button is clicked:
mutate()
render()
jsx
And we get something like this:
Video Alt:
- On the left side, it is shown that the button element has an onclick attribute that run
mutate()
andrender()
. - Whenever a user clicks the button, the number will increase by one
3 Actions
Before we continue, we have these 3 actions that we break down earlier:
- Declare → initialize variable using let
- Mutate → change the count variable
- Render → update changes to the screen
Let's split the button into its own functions, so you can see it clearly.
<h1>Counter</h1>
<p id="count">0</p>
<button onclick="mutate()">Mutate</button>
<button onclick="render()">Render</button>
<script>
let count = 0;
function mutate() {
count++;
logTime();
console.log('clicking, count: ', count);
}
function render() {
document.getElementById('count').textContent = count;
}
</script>
html
Video Alt:
- When the mutate button is clicked, the console shows the count is increasing. However, the number on the screen doesn't change at all.
- After the render button is clicked, the number on the screen changes to the last count value.
Looking at React
By bluntly translating the JavaScript code, this is what we have now.
function Component() {
let count = 0;
function mutate() {
count = count + 1;
console.log(count);
}
return (
<div>
<h1>{count}</h1>
<button onClick={mutate}>Mutate</button>
</div>
);
}
tsx
Do you see something odd?
found it?
Yes, there is no render function.
We can, of course, use the same render function by accessing document
, but it's not a good practice to access them manually on React, our purpose of using React is not to manage them manually.
Render Function
What is the equivalent of render function in React then?
It is actually the function Component()
itself.
Whenever we want to update the screen, React are calling Component()
function to do that.
By calling the function, the count
is declared again, the mutate
function is also re-declared, and at last, will return a new JSX.
Here is the demo:
Video Description:
- We can see that there are 2 console log in the line 13 and 15
- When the page is reloaded, the console logs are running. (this is normal behavior as the initial render)
- Every time the Re-render button is clicked, the logs are called. This proves that the Component() is called every render.
What triggers render function?
If we run the code with let on React, there will be no changes. That's because the render function doesn't get called.
React will trigger render function:
- When the useState value changes (using setState)
- When the parent component re-renders
- When the props that are being passed changes
The second and the third are basically triggered because of setState too but in the parent element.
At this point, we know that every time the useState value changes, it will call the render function which is the Component function itself.
Simulating the render function
Before we convert the count
variable to state, I want to demonstrate by creating a render function simulation, which uses setToggle. We can trigger re-render with render
now.
function Component() {
//#region //*=========== Render Fn Simulation ===========
const [toggle, setToggle] = React.useState<boolean>(false);
function render() {
setToggle((t) => !t);
}
//#endregion //*======== Render Fn Simulation ===========
let count = 0;
const mutate = () => {
count = count + 1;
console.log(`${getTime()}| count: ${count}`);
};
return (
<div>
<h1>{count}</h1>
<Button onClick={mutate}>Mutate</Button>
<Button onClick={render}>Render</Button>
</div>
);
}
tsx
Let's see it in action
Video Alt:
- Mutate button is clicked, and the count is incremented to 4
- Render button is clicked, but the number on screen doesn't change, while the console log is 4.
- Render function is clicked again, the number on screen is still 0, while the console log change to 0
- After mutate is clicked, it increments, but not from 4, it increments starting from 0 again.
🤯 Why is it not working?
This is actually because we are re-declaring the count variable.
function Component() {
//#region //*=========== Render Fn Simulation ===========
const [toggle, setToggle] = React.useState<boolean>(false);
function render() {
setToggle((t) => !t);
console.log(`${getTime()} | Render function called at count: ${count}`);
}
//#endregion //*======== Render Fn Simulation ===========
let count = 0;
const mutate = () => {
count = count + 1;
console.log(`${getTime()}| count: ${count}`);
};
return (
<div>
<h1>{count}</h1>
<Button onClick={mutate}>Mutate</Button>
<Button onClick={render}>Render</Button>
</div>
);
}
tsx
Every time react calls the Component function, we are re-declaring the count to be 0. The render function still works, and react updated the screen, but it updated to the re-declared count which is still 0.
Now that is why we can't use a normal variable in a React component.
Declaring Outside of Component
You might also ask:
Why don't we move the declaration outside the Component function?
Well, it makes sense, by moving the declaration we are avoiding the count
being re-declared to 0. Let's try it to be sure.
let count = 0;
function Component() {
//#region //*=========== Render Fn Simulation ===========
const [toggle, setToggle] = React.useState<boolean>(false);
function render() {
setToggle((t) => !t);
console.log(`${getTime()} | Render function called at count: ${count}`);
}
//#endregion //*======== Render Fn Simulation ===========
const mutate = () => {
count = count + 1;
console.log(`${getTime()}| count: ${count}`);
};
return (
<div>
<h1>{count}</h1>
<Button onClick={mutate}>Mutate</Button>
<Button onClick={render}>Render</Button>
</div>
);
}
tsx
Video Alt:
- Mutate button is clicked 3 times, and the
count
is incremented to 3 - Render button is clicked, and the number on the screen updated to 3
- When the mutate button is clicked again, the increment continues from 3 to 5
- When the render button is clicked again, it is updated to the correct count.
IT WORKS! or is it?
It did just work, that was not a fluke. But there is something you need to see.
Video Alt:
- Current count is = 5, it is proven by clicking the render button, it's still 5.
- Then, we move to another page
- Back to the counter page, but the count is still 5
- Clicking the mutate button will increment from 5
Yes, the variable doesn't get cleared.
This is not great behavior, because we have to manually clean it or it will mess up our app.
Now that is why we can't use a normal variable outside a React component.
Using useState
This is the code if we are using useState
function Component() {
const [count, setCount] = React.useState<number>(0);
const mutateAndRender = () => {
setCount((count) => count + 1);
console.log(`${getTime()} | count: ${count}`);
};
return (
<div>
<h1>{count}</h1>
<div className='mt-4 space-x-2'>
<Button onClick={mutateAndRender} variant='light'>
Add
</Button>
</div>
</div>
);
}
tsx
And this is the demo
Video Alt:
You may notice that the console.log count is late by 1, ignore it for now.
- Add button is clicked, then the count is added, and simultaneously updated on screen
- When moving to another page and back, the count is reset back to 0.
So in recap, useState does 4 things:
-
Declaration, by declaring using this syntax
const [count, setCount] = React.useState<number>(0);tsx -
Mutate and Render, changing the value and automatically render the changes using
setCount
-
Persist the data in each re-render → when the render function is called, useState won't re-declare the count value.
-
Reset the value when we move to another page, or usually called: when the component unmounts.
Why the count is late
const mutateAndRender = () => {
setCount((count) => count + 1);
console.log(`${getTime()} | count: ${count}`);
};
tsx
This is because the setCount
function is asynchronous.
After we call the function, it needs time to update the count value. So when we call the console.log immediately, it will still return the old value.
You can move the console.log outside of the function so it will run on re-render (Component()
)
function Component() {
...
const mutateAndRender = () => {
setCount((count) => count + 1);
};
console.log(`${getTime()} | count: ${count}`);
return ...
}
tsx
3 Actions Diagram
Here is the updated diagram, now you know what useState and setState do.
Test your knowledge
Quiz 1
Please take a look at this snippet for the next 2 questions.
import * as React from 'react';
function Component() {
let count = 0;
const mutate = () => {
count = count + 1;
console.log(count);
};
return (
<div>
<h1>{count}</h1>
<Button onClick={mutate}>Mutate</Button>
</div>
);
}
tsx
After clicking the Button twice, what is the value of the console.log?
Go ahead and pick one!
How about the value on screen within the h1 tag?
Go ahead and pick one!
Quiz 2
Now, we use some render function simulation, you should be able to answer this one correctly 💪
import * as React from 'react';
function Component() {
//#region //*=========== Render Fn Simulation ===========
const [toggle, setToggle] = React.useState<boolean>(false);
function render() {
setToggle((t) => !t);
}
//#endregion //*======== Render Fn Simulation ===========
let count = 0;
const mutate = () => {
count = count + 1;
console.log(count);
render();
};
return (
<div>
<h1>{count}</h1>
<Button onClick={mutate}>Mutate</Button>
</div>
);
}
tsx
After clicking the Button twice, what is the value of the console.log?
Go ahead and pick one!
How about the value on screen within the h1 tag?
Go ahead and pick one!
Recap
Great job, you have finished the first React Core Concept Series. I'll definitely continue this series as there are still many hooks to cover. Please hold on to the mental model that I put in this blog post, as I'm going to reference it again soon on the next post.
With this post, we learned that
- We can't use normal let because React calls the Component function itself to do the re-rendering.
- Re-rendering will cause all of the code in the Component function to be run all over again including the variable and function declaration, as well the console logs and function calls.
- Using the useState hook will help us update the variable, and the number on the screen while still persisting the data between re-renders.
See you in the next blog post. Subscribe to my newsletter if you don't want to miss it.