Introduction
Understanding state management is fundamental in React development. The arrival of Hooks in React 16.8 transformed how React developers manage state, with the useState
hook being the most critical to learn and the most useful to adopt in your React components. In this blog post, we’ll look through React’s useState
hook — the basics, including setting up your first hook and interacting with it, all the way to the advanced functionality.
Before the introduction of Hooks, which includes useState
, managing state in React was mainly done through class components. When developers needed local state within a component, they would have to declare the component as a class.
In the class component, the state was initialized in the constructor method:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
incrementCount = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<button onClick={this.incrementCount}>
Clicked {this.state.count} times
</button>
);
}
}
React state was managed through this state
property and the setState
method. setState
was used to update the component state and trigger a re-render.
While this worked well, it required the use of JavaScript classes, which could sometimes add unnecessary complexity. The introduction of Hooks and useState
simplified state management by enabling it within function components, removing the need for classes and complex lifecycle methods.
Creating Your First useState Hook
The useState hook is a built-in function in React that allows you to add, use and manipulate local state inside your functional components. In a nutshell, a Hook is a unique function that lets you “hook into” React features. For example, useState is a Hook that allows you to add stateful logic into a function component.
Let’s dive in by creating a simple counter using useState
.
Firstly, import the Hook at the top of your file:
import React, { useState } from 'react';
Next, initiate your useState hook within your function component. It takes one argument—the initial state—and returns an array of two values: the current state and a function to update that state. These values can be called whatever you’d like, but should be named something that helps indicate what the piece of data you’re working with is:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
Here, count
is the state variable, and setCount
is the function to update this state. Whenever you press the button, setCount
is triggered, increasing the counter state by one. Note that unlike the prior class component/setState
code, there’s no additional functions and no constructors needed. The parameters returned from useState
can be used anywhere inside of the current JavaScript scope.
Understanding Function Argument Syntax
As you begin using useState
in more complex situations, you may find that your state becomes "out of date". One of the most common situations that developers will run into this is when you "batch" updates to setState
:
const doUpdate() => {
console.log(count) // 0
setCount(count + 1)
console.log(count) // 1
// do other stuff
setCount(count + 1)
console.log(count) // sometimes 2... sometimes 1! ack!
}
This can happen because React will attempt to "batch" multiple setState
calls in order to improve your application’s performance.
The other situation that this behavior is undesired is when you are relying on the previous state value while calling the state update function. For instance, if you are appending values to an array:
console.log(fruits) // ["apple"]
setFruits([
...fruits,
"banana"
])
// ... later
setFruits([
...fruits,
"orange"
])
console.log(fruits) // ["apple", "orange"]
To fix this, you can use the alternative function syntax for useState
:
const [fruits, setFruits] = useState([]);
setFruits(prevFruits => [
...prevFruits,
"banana"
]} // ["apple", "banana"]
setFruits(prevFruits => [
...prevFruits,
"orange"
]} // ["apple", "banana", "orange"]
The reason this fixes the previous issues with useState
is the usage of the "previous state" function argument (prevFruits
in the above example). The function inside setFruits
gets the previous state as its argument, which ensures you are updating the state based on the correct previous state.
Complex Data Structures in useState
What if our state is an object or an array rather than a simple data type, like a number or string? No problem! useState perfectly handles complex data structures.
const [form, setForm] = useState({ username: '', password: '' });
const updateForm = e => {
setForm({
...form,
[e.target.name]: e.target.value,
});
};
// In Form field
<input
type="text"
name="username"
value={form.username}
onChange={updateForm}
/>
Here’s an example of state as an object where we are maintaining a form with username and password fields. We use the spread operator (...
) to clone the old state before updating it. In general, you should be using immutable functions to modify the data inside of a setState
call. This is because React can have issues understanding that a change has occurred inside of a complex state value that it is tracking unless you make an immutable update to the value.
useState
is super important for React developers to understand. While the basics can be grokked in just a few minutes, the complex interactions and alternative syntaxes are worth learning so that you aren’t left confused when it doesn’t work like you expected.