An in-depth explanation of the React ‘useState’ hook

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!

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!