In this article, we'll thoroughly explore the React useState
hook, a fundamental tool to manage state in React functional components. We'll cover both the basics and the advanced concepts and point out the best practices for using this hook effectively. Let's get started.
The basics
The useState
hook optionally takes an argument, which can be any data type, and returns an array containing the current state and a setter function to update the state. The convention is to destructure the array and pull the state and the setter function as variables.
Important to note, that the first variable in the destructured array must be the state, and the second one must be the setter function, as position matters in array destructuring. Another naming convention is to take the state variable name and prefix it with the ‘set’ keyword to name the setter function.
Once you create a piece of state using the useState
hook, you can use that state throughout a component and pass it as a prop to a child component. Whenever you need to update the state, you use the setter function. Enough theory, let's take a look at an example.
const [count, setCount] = useState(0);
console.log(count); // 0
function increase() {
setCount((prevState) => prevState + 1);
}
In the above example, we've destructured the count
variable and the setCount
function from the useState
hook. We've also used 0 as the argument of the useState
function, which serves as the initial state and becomes the value of the count
variable.
Moving forward in the code, we've created the increase
function that uses the setCount
function to update the state. The setCount
function takes a callback function as an argument that receives a parameter prevState
(the previous state) and returns the current state by adding 1 to it.
Now, every time you call the increase
function, the state will increase by 1, and the change will reflect everywhere you've used the count
variable.
The advanced
An important note about the setter function of the useState
hook is that it's asynchronous. This means that the setter function doesn't update the state immediately as it's executed. Let's examine this:
const [count, setCount] = useState(0);
function increase() {
setCount((prevState) => prevState + 1);
console.log(count); // 0
}
As you can see in the above code, although the increase
function executes the setCount
function, which updates the state, the value of the count
variable remains 0 in the next line.
It demonstrates that the setter function of the useState
hook is asynchronous. This brings us to the next important topic, which is that you should always use a callback function and the previous state to perform state updates. Let's understand why:
const [count, setCount] = useState(0);
function increase() {
setCount(count + 1);
// Or
setCount(() => count + 1);
}
console.log(count); // 1
Suppose you want to update the state twice each time you call the increase
function. So, you call the setter function twice, adding 1 with the count
variable each time, as shown in the above example. But this doesn't work as expected, and the state updates only by 1. Why is it so?
Behind the scenes, when the first setter function runs, React creates an asynchronous task with the value of the count
variable as 0. In the next line, the setter function runs again, and React creates another asynchronous task, with the value of the count
variable still being 0, as the first asynchronous task hasn't been executed yet.
By the way, if you are interested, I have an article on Exploring the inner workings of a JavaScript runtime that provides further insights into how asynchronous tasks are created and executed in a JavaScript runtime.
Next, the first asynchronous task is executed, which finds the value of the count
variable as 0 and updates the state by adding 1 to it. Then the second asynchronous task is executed, which also finds the value of the count
variable as 0 and updates the state by adding 1 to it. As a result, the state remains 1 instead of 2, which doesn't fulfill our intention.
This demonstrates that while this approach of updating state might work in some cases, it's not the correct way to do so. Instead, the correct method is to use a callback function and the previous state to perform state updates, as shown in the following example:
const [count, setCount] = useState(0);
function increase() {
setCount((prevState) => prevState + 1);
setCount((prevState) => prevState + 1);
}
console.log(count); // 2
As you can see, we've used a callback function and the previous state to update the state in the above example. Notice that the callback function receives a parameter prevState
(the previous state) and returns the current state by adding 1 to it.
Behind the scenes, when the increase
function is executed, React creates two asynchronous tasks. But this time without a constant value. Next, the first asynchronous task is executed, which finds the previous state as 0 and updates the current state by adding 1 to it. Then, the second asynchronous task is executed, which finds the previous state as 1 and returns the current state by adding 1 to it, which achieves the desired result.
This concludes our discussion of the React useState
hook. I hope you now understand the basics of the useState
hook, the asynchronous nature of the setter function, and the importance of using a callback function and the previous state for state updates. Happy hacking!