React useState Hook

Summary: in this tutorial, you will learn about React useState() hook and how to use it effectively to make your component more robust.

Introduction to React useState() hook

In React, a state is a piece of data that changes over time. To add a state variable to a component, you use the useState() hook.

First, import the useState is a function of the react library:

import { useState } from 'react';Code language: JavaScript (javascript)

Second, use the useState() function in your component:

const [state, setState] = useState(initialValue);Code language: JavaScript (javascript)

The useState() function returns an array with exactly two elements:

  • A variable (state) that holds the current state value. In the first render, the state variable has a value of initialSate.
  • A function (setState) that allows you to change the current value of the state variable to a new one and trigger a re-render.

By convention, the function name that updates the state variable starts with the verb set followed by the variable name.

For example, if the state variable is count, then the function to update the count variable is setCount:

const [count, setCount]  = useState(0);Code language: JavaScript (javascript)

The useState() function accepts an initial value of any valid data type (string, number, array, object, …).

Updating state

There are two ways to call the setState() function to update a state variable:

  • Direct update.
  • Functional update.

In a direct update, you don’t need to know the previous state and pass a new value directly to the setState() function. For example:

setCount(1);Code language: JavaScript (javascript)

In this example, the setCount() function will set the new count value to 1 regardless of the current state value.

In a functional update, you pass a function that accepts the previous state as the input argument and returns the new state. For example:

setCount(prevCount => prevCount + 1);Code language: JavaScript (javascript)

In this example, the setCount() function increments the count variable by one based on the previous count value.

The useState hook example

Let’s take a basic example of using the useState hook in a Counter component:

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>Current count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;Code language: JavaScript (javascript)

In this example, the following uses the useState() hook to declare the count variable with an initial value of zero:

const [count, setCount] = useState(0);Code language: JavaScript (javascript)

When the Increment button is clicked, the increment() function is invoked, which uses the setCount() function to update the count variable.

Because the count variable changes, React triggers a component re-render and displays the new count value.

State and component re-renders

In React, a component rerenders whenever its state changes by calling the setState() function. To monitor the component rerenders, you can use a useEffect() hook as follows:

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Component rendered or rerendered');
  });

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>Current count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;Code language: JavaScript (javascript)

Whenever the Increment button is clicked, the count variable is incremented by one, which triggers a component re-render. You’ll see the message Component rendered or rerendered at the first render and in every re-render on the console window

If the new state value is the same as the previous one, React doesn’t rerender the component.

For example, if you call the setCount() function but do not change the count variable, React will not trigger a rerender:

const increment = () => {
   setCount(count);
};Code language: JavaScript (javascript)

On the console window, you’ll see one message component rendered or rendered the first time the component renders, and won’t see the same message when the Increment button is clicked.

You can log the count value immediately after updating it. But the log will show the count value before the update:

const increment = () => {
    // update the count
    setCount(count + 1);

    // show the count before the update
    console.log(count);
};Code language: JavaScript (javascript)

In the increment() function, we increment the count value by one:

setCount(count + 1);Code language: JavaScript (javascript)

React does not immediately rerender the component. Instead, it schedules for an update. Therefore, the value of the count variable current count value before the update takes effect.

It’s important to note that React batches multiple state updates for performance reasons. This is why you will not see the new state immediately in the console.

For example, you can call the setCount() function multiple times, and React will batch these updates, resulting in a single re-render:

const increment = () => {
    // update the count multiple times
    setCount(count + 1);
    setCount(count + 2);
    setCount(count + 3);

    // show the count before the update
    console.log(count);
};Code language: JavaScript (javascript)

In this example, React will schedule three state updates, which results in a single re-render. And you’ll see the count values on the console window like 0, 3, 6, …

The reason is that the setCount() will be called three times based on the current count value:

  • First Update: count + 1 – Increases the count by 1.
  • Second Update: count + 1 – Increases the current count (not the updated count) by 1.
  • Third Update: count + 3 – Increases the current count (not the latest count) by 1.

If the current count is 0, then three calls set the count to 3. If the current count is 3, the three calls set the count to 6, and so on.

To update a state based on the previous state, you should use the functional update which takes the previous state as the argument. For example:

const increment = () => {
  // update the count multiple times
  setCount((prevCount) => prevCount + 1);
  setCount((prevCount) => prevCount + 2);
  setCount((prevCount) => prevCount + 3);

  // show the count before the update
  console.log(count);
};Code language: JavaScript (javascript)

In this example:

  • First Update: prevCount + 1 – Increases the count by 1.
  • Second Update: prevCount + 2 – Increases the updated count by 2.
  • Third Update: prevCount + 3 – Increases the latest count by 3.

React will batch these updates with the final state as the prevCount + 6. So you’ll see 0, 6, 12, 18, etc. on the console window.

Handling complex state

When a component has an array or object state, you should handle it carefully to ensure immutability.

Updating array states

When dealing with an array state, you should create a copy of the original array with the modified item when updating it to avoid changing the array directly.

Let’s take a look at the following FruitList component:

import React, { useState } from 'react';

const commonFruits = [
  '🍎 apple',
  '🍌 banana',
  '🍒 cherry',
  '🍈 date',
  '🥭 mango',
  '🍇 grape',
  '🥝 honeydew',
  '🥥 coconut',
  '🍋 kiwi',
  '🍊 lemon',
  '🍉 warter melon',
  '🍊 orange',
  '🍑 peach',
];

const FruitList = () => {
  const [fruits, setFruits] = useState([]);
  const addFruit = () => {
    const newFruit =
      commonFruits[Math.floor(Math.random() * commonFruits.length)];
    setFruits([...fruits, newFruit]);
  };
  return (
    <div>
      <h1>Fruit List</h1>
      <ul>
        {fruits.map((fruit, index) => (
          <li key={index}>{fruit}</li>
        ))}
      </ul>
      <button onClick={addFruit}>Add Fruit</button>
    </div>
  );
};

export default FruitList;
Code language: JavaScript (javascript)

The FruitList component has the fruits state which is an array. The useState hook initializes it to an empty array:

const [fruits, setFruits] = useState([]);Code language: JavaScript (javascript)

To add a new element to the fruits array we create a new array by copying existing fruits and adding a new fruit using the following syntax:

setFruits([...fruits, newFruit]);Code language: JavaScript (javascript)

Notice that if you use the push() method to add an item to the array, you still refer to the same array reference even though the number of items of the array changes. In this case, React will not trigger a component rerender:

// DON'T DO THIS
fruits.push(newFruit);
setFruits(fruits);Code language: JavaScript (javascript)

Updating object states

Like array states, when updating object states, you should ensure that you create a new object rather than mutating the existing one. For example:

import React, { useState } from 'react';

const Person = () => {
  const [person, setPerson] = useState({
    name: 'John',
    age: 20,
  });

  const handleIncrement = () => {
    setPerson({ ...person, age: person.age + 1 });
  };

  return (
    <div>
      <p>Name: {person.name}</p>
      <p>Age: {person.age}</p>
      <button onClick={handleIncrement}>Increment Age</button>
    </div>
  );
};

export default Person;Code language: JavaScript (javascript)

In this example, we create a new Person object by copying the existing properties of the original object and updating the age property:

setPerson({ ...person, age: person.age + 1 });

Lazy Initialization of State

Sometimes, initializing the state might involve an expensive calculation. To improve the performance, React allows you to initialize the state lazily by passing a function to useState(). The useState() will call this function only once during the initial render.

In the following Theme component, the useState() hook calls the getTheme() function every time the component re-render, which is not necessary:

import React, { useState } from 'react';

function getTheme() {
  console.log('Getting the theme from the local storage');
  return localStorage.getItem('theme') || 'light';
}

const Theme = () => {
  const [theme, setTheme] = useState(getTheme());
  const handleClick = () => {
    console.log('Changing the theme');
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);
  };
  return (
    <div>
      <p>Current Theme: {theme}</p>
      <button onClick={handleClick}>Swich Theme</button>
    </div>
  );
};

export default Theme;Code language: JavaScript (javascript)

To instruct the useState hook to call the getTheme() function only once during the initial re-render, you can pass a function to it like this:

const [theme, setTheme] = useState(() => getTheme());Code language: JavaScript (javascript)

You can use this lazy state initialization technique when you have to initialize a state that requires a computation you don’t want to repeat on every render, or when the initial state involves retrieving data from external sources like localStorage.

Summary

  • Call the useState() hook to add a state variable to the component.
  • The useState() function returns an array of two items: a state variable (state) and a function for updating the state (setState).
  • Always update the state variable by using the setState function. The setState function will trigger a component re-render if the state changes.
  • Return a new copy of an array or object instead of directly modifying it when updating a complex state.
  • Use lazy initialization if the initial state is expensive to calculate.
Was this tutorial helpful ?