Codementor Events

The useState hook - A comprehensive guide

Published Aug 12, 2022
The useState hook - A comprehensive guide

What is a state?

Before we dive deep into the useState hook let’s first understand the term state.

State represents information about something at a given point in time.

For example, let’s consider a textbox.

Initially, there is nothing inside this textbox, so its state is empty. Suppose you start typing Hello inside it, for each key stroke the state of the textbox will change. At first it will be “H”, then “He”, then “Hel” and so on until it becomes “Hello”.

Also, notice that as you’re typing, you’re not losing the previous value. If you press “H” followed by “e” you’re getting “He” and not just “e”. In other words you can think of state as the memory of the textbox.

The need for state in a React component.

Let’s understand this with the help of an example.

Codesanbox without state

Here we have a ClickCounter component which displays the number of times the Increment button was clicked.

We are using a local variable “counter” to keep count of the clicks.

Each time we click the Increment button, handleClick function will be invoked. This function will increase the counter value by 1 and also log the value in the console.

Go ahead, click the Increment button in CodeSandbox preview.

Nothing happened?

Well our logic seems to be correct. The value logged in the console (CodeSandbox) updates correctly each time we click, but why isn’t this update reflected in the UI?

It’s because of the way React works.

  • Changes to local variables do not trigger a re-render.
  • During a re-render, a component is created from scratch i.e. a component’s function (in this example it’s the ClickCounter function) is executed once again. Since variables(for example, counter) are local to the function, their previous values are lost.

So how do we make the component remember values between renders?

Gif coming to an answer

Yes, you got it right! We do this with the help of the useState hook.

The useState hook

The useState hook provides mechanisms to preserve the state and trigger a re-render.

Let’s look at its usage.

import React, { useState } from "react";
const state = useState(initialValue);

// OR

const state = React.useState(initialValue);

The useState hook returns an array which contains two items:

  • A state variable that retains its values during re-renders. The initial value passed to useState is assigned to the state variable during the first render.
  • A setter function that updates the state variable and also triggers a re-render.
const state = useState(0);
const data = state[0];
const setData = state[1];

Using array de-structuring , we can refactor the above statements into a single statement, as shown below:

const [data, setData] = useState(0);

The initial value passed to useState is used only during the first render. For re-renders, it is ignored.

Counter with useState

Now, let’s update the earlier counter example to include the useState hook.

  • Since we need the counter value between re-renders, let’s convert it into a state.
const [counter, setCounter] = useState(0);
  • Calling setCounter inside the handleClick function.
const handleClick = () => {
  setCounter(counter + 1);
  console.log(`%c Counter:${counter}`, "color:green");
};

The setCounter function will update the counter value by 1 and trigger a re-render. When the component’s function is called on re-render the state variable returned by useState will have the updated value.

Try out the CodeSandbox with the updated code. Click the Increment button and see the magic of useState in action.

Codesanbox with useState

You can verify that on a re-render, the functional component ClickCounter is called again by viewing the console logs. The log “ClickCounter start” which is added in the beginning of the component will be logged on each render.

first render

re-render

Updater function

Suppose we want to increase the value of counter by 4 on each click.

const handleClick = () => {
  setCounter(counter + 1);
    setCounter(counter + 1);
    setCounter(counter + 1);
    setCounter(counter + 1);
    console.log(`%c Counter:${counter}`, "color:green");
    };

Assume that the initial value of counter is 0. What do you expect to see once the button is clicked?

Without updater function

You expected the count to be 4 right? But why do you see 1 instead?

a) Each render is associated with a state. The value of that state remains locked for the lifetime of that render.

Notice that the log inside the handleClick function prints the counter value as 0.

No matter how many times you call the setCounter method, the value of counter remains the same.

const handleClick = () => {
  setCounter(counter + 1);
    setCounter(counter + 1);
    setCounter(counter + 1);
    setCounter(counter + 1);
    console.log(`%c Counter:${counter}`, "color:green");
    };
b) Until all the code inside an event handler is executed, React will not trigger a re-render.

For this reason each setCounter call doesn’t trigger an individual render. Instead React adds these setter functions in a queue. Executes them in the order they were queued. The updates made to the state after executing all the statements is reflected in the next render. This queueing of multiple state updates is known as batching. It allows React to be more performant.

Therefore, here we get a single render instead of 4 different renders.

This example is simple and you can fix this issue by updating the code as shown below:

const handleClick = () => {
setCounter(counter + 4);
    console.log(`%c Counter:${counter}`, "color:green");
    };

But what if you had a use case where you wanted to update the state multiple time before the next render.

That is where the _ updater _ function comes handy.

We can refactor the previous example with the updater function as follows:

const handleClick = () => {
  setCounter(prevCounter => prevCounter + 1);
    setCounter(prevCounter => prevCounter + 1);
    setCounter(prevCounter => prevCounter + 1);
    setCounter(prevCounter => prevCounter + 1);
    console.log(`%c Counter:${counter}`, "color:green");
    };

Here prevCounter ⇒ prevCounter + 1 represents the updater function.

As explained earlier, these updater statements are also queued (batching).

The updater function receives a pending/previous state which it uses to calculate the next state.

Updater function batching

Below is the CodeSandbox with the updater function added. Try clicking on the increment button.

Updater function sandbox

Initializer function

Take a look at the example below. Here we are calling getItems function to get the initial value for the state.

import React from "react";
import { useState } from "react";
function ListItems() { 
  const getItems = () => { 
    console.log(`%c getItems called`, "color:hotpink");
    	return Array(50).fill(0); 
    }; 
  const [items, setItems] = useState(getItems()); 
    
    return ( 
    	<div className="card">
        <ul> {items.map((item, index) => 
        	( <li key={index}>Item {index + 1}		</li>))} 
        </ul> <button onClick={() => setItems([...items, 0])}>Add Item</button> 	</div> );
} 
export default ListItems;

This function creates an array with size 50 and fills the array with zeros. Refer the image below.

Array filled with 50 zeros

These items are then displayed on the screen.

Everything seems to be fine but we have a problem here.

Click on the Add Item button(located after the list of items) to add a new item to the list. Observe the logs.

Without initializer function

Do you see the problem here?

The log “getItems called” gets added to the console everytime you add an item. This means that this function is being called on each render.

Remember that useState ignores the initial value passed to it after the first render, but here the initial value is still being re-calculated. This can be expensive if we’re creating large arrays or performing heavy calculations.

We can solve this issue by passing getItems as an _ initializer _ function.

Now, let’s make a small change to the code.

const [items, setItems] = useState(getItems);

With initializer function

See the console window in CodeSandbox. Notice that “getItems called” log is only printed on the first render. When subsequent items are added this log isn’t there.

Although there isn’t a visual difference between the two examples, in terms of performance they’re different.

Remember when you need a function for the initial state, always pass the function or call the function inside another function. Never call the function directly.

✅ const [items, setItems] = useState(getItems);
✅ const [items, setItems] = useState(() => getItems());
❌ const [items, setItems] = useState(getItems());

How many useState hooks can I have

You can have as many useState hooks inside a component as it requires.

See the CodeSandbox

Multiple useState hooks

The component below has three different states - username, password, keepMeSignedIn.

Try updating the values of username, keepMeSignedIn. The updated states are logged in the console when login button is clicked.

Highlights

  • useState provides a mechanism to trigger a re-render and persist the state between re-renders.
  • Use the updater function when you need to:
    • Calculate the next state based on the previous state.
    • Perform multiple updates to the state before the next render.
  • If initial state is obtained from a function - use the initializer function syntax.
  • There can be multiple useState hooks inside a component.

Liked this post? Share it with others.
Orignally written for my personal blog - https://gauravsen.com/use-state-hook

Discover and read more posts from Gaurav Sen
get started