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, thestate
variable has a value ofinitialSate
. - A function (
setState
) that allows you to change the current value of thestate
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. ThesetState
function will trigger a component re-render if thestate
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.