How to Use the useState() Hook in React – Explained with Code Examples
The useState
hook is one of the most important tools in a React developer‘s toolkit. It allows functional components to have state, which is critical for building dynamic, interactive user interfaces.
According to the State of JS 2022 survey, React is used by over 80% of front-end developers, and hooks are used by 94% of React developers. Understanding how to use useState
effectively is a must-have skill.
In this in-depth guide, we‘ll cover everything you need to know to master the useState
hook, including:
- What state is and why it‘s important
- How to use the
useState
hook - Updating objects and arrays in state
- Working with multiple state variables
- Best practices and common pitfalls
- Related hooks and resources
Let‘s get started!
What is State and Why Does It Matter?
In React, state refers to data that can change over time and affect what is rendered on the screen. Some common examples of state include:
- Form input values
- Current selected tab or accordion item
- Data fetched from an API
- Whether a modal is open or closed
Whenever the state changes, React automatically re-renders the component to reflect the new state. This declarative approach to UI development is what makes React so powerful and popular.
Before hooks were introduced in React 16.8, only class components could have state. Functional components were limited to receiving props and returning JSX. The introduction of hooks like useState
changed that, allowing developers to add state to functional components.
How to Use the useState Hook
Using the useState
hook is straightforward. Here are the key steps:
- Import
useState
from thereact
package - Call
useState
inside your functional component, passing the initial state value - Destructure the returned array into a state variable and a function to update it
- Use the state variable in your JSX
- Call the update function to change the state value as needed
Here‘s a simple counter example putting this all together:
import React, { useState } from ‘react‘;
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
In this example:
- We import
useState
fromreact
- Inside the
Counter
component, we calluseState(0)
to initialize a state variablecount
to 0 - We destructure the returned array into
count
andsetCount
variables - We display the current
count
value in a<p>
tag - When the button is clicked, we call
setCount(count + 1)
to increment the count by 1
Every time setCount
is called, React will re-render the Counter
component, updating the displayed count value.
Updating Objects and Arrays in State
In addition to primitive values like numbers and strings, you can also store objects and arrays in state using useState
. However, updating them requires a bit more care.
Consider an example where we have an array of todo items in state:
import React, { useState } from ‘react‘;
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: ‘Learn React‘, completed: false },
{ id: 2, text: ‘Build a project‘, completed: false },
{ id: 3, text: ‘Write an article‘, completed: false },
]);
function toggleTodo(id) {
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
setTodos(updatedTodos);
}
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? ‘line-through‘ : ‘none‘ }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</li>
))}
</ul>
);
}
In this example, we have an array of todo objects in state. When a todo is clicked, we want to toggle its completed
status.
However, we can‘t simply mutate the todos
array directly, like this:
function toggleTodo(id) {
const todoToUpdate = todos.find((todo) => todo.id === id);
todoToUpdate.completed = !todoToUpdate.completed; // Don‘t do this!
setTodos(todos);
}
This mutates the state directly, which can lead to unexpected bugs. Instead, we need to create a new array with the updated todo object.
The toggleTodo
function does this by:
- Using
map
to loop through thetodos
array - If the current
todo.id
matches theid
passed to the function, returning a new object with thecompleted
status flipped - Otherwise, returning the original
todo
object unchanged - Passing the new
updatedTodos
array tosetTodos
By creating a new array instead of mutating the existing one, we ensure that React correctly detects the state change and re-renders the component.
The same principle applies when updating objects in state. Always create a new object instead of mutating the existing one.
Working with Multiple State Variables
As your components grow in complexity, you may find yourself needing to manage multiple pieces of related state. There are a few different approaches you can take.
Using a Single State Object
One option is to store all related state in a single object, like this:
import React, { useState } from ‘react‘;
function Form() {
const [form, setForm] = useState({
name: ‘‘,
email: ‘‘,
message: ‘‘,
});
function handleChange(event) {
const { name, value } = event.target;
setForm((prevState) => ({
...prevState,
[name]: value,
}));
}
function handleSubmit(event) {
event.preventDefault();
console.log(form);
// Submit form data to server...
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
/>
</label>
<label>
Message:
<textarea
name="message"
value={form.message}
onChange={handleChange}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
In this example, we store the name
, email
, and message
form fields in a single form
state object.
The handleChange
function uses the name
attribute of the input that triggered the change event to dynamically update the corresponding state property using computed property names.
While this approach works, it can become cumbersome as the number of fields grows.
Using Multiple State Variables
An alternative approach is to use a separate state variable for each form field:
import React, { useState } from ‘react‘;
function Form() {
const [name, setName] = useState(‘‘);
const [email, setEmail] = useState(‘‘);
const [message, setMessage] = useState(‘‘);
function handleSubmit(event) {
event.preventDefault();
console.log({ name, email, message });
// Submit form data to server...
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<label>
Email:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label>
Message:
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
With this approach, each form field has its own state variable and update function. The onChange
handlers are inlined directly in the JSX.
This can make the JSX a bit more verbose, but it‘s often easier to reason about and maintain, especially as the form grows larger.
Ultimately, the approach you choose depends on your specific use case and personal preference. Both are valid ways to manage form state with useState
.
Best Practices and Common Pitfalls
Here are a few best practices and gotchas to keep in mind when working with useState
:
Deceptively Simple State
It‘s easy to fall into the trap of thinking that all your state should be simple primitives like strings and numbers. In reality, many components need to manage more complex state shapes like objects and arrays.
Don‘t be afraid to use objects and arrays in state when it makes sense for your component. Just remember to update them immutably.
Keep State as Localized as Possible
In general, you want to keep state as close to where it‘s being used as possible. Don‘t hoist state up to a parent component unless it‘s truly necessary.
Lifting state up
Component did update life cycle
Naming conventions for state variables
Resources for going deeper (React docs, Dan Abramov blog post, Dave Ceddia etc)