February 14, 2024 - Edited on May 27, 2024
useState
is arguably the most used React Hook.
It allows you to add interactivity to your components & make your website more engaging.
In this post, you’ll learn everything you need to know about the useState
hook, with interactive examples and best
practices.
Goal
useState
allows you to define a variable inside your component.
The state can then be used to render the UI.
Usage
Using useState
is pretty straightforward.
To use it in your application follow these three steps:
1. Import
To get started, import useState
:
import { useState } from "react";
2. Definition
Then define a state inside your component:
function MyComponent() { const [name, setName] = useState(""); // ... }
Here we define a state called name
with an initial value of ""
(empty string).
Here's the basic syntax:
In a nutshell, useState
takes the initial value as an argument and returns a variable
and an update function to set the value.
3. Binding
The last step is actually using the state. In other words, bind your state to the UI.
return ( <input type={"text"} value={name} onChange={(e) => setName(e.target.value)} /> )
Putting it all together:
You entered: Youssef
import { useState } from "react";
export function Component() {
const [name, setName] = useState("Youssef");
return (
<div>
<label>Enter your name</label>
<input
type={"text"}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<br />
<p>
You entered:{" " + name}
</p>
</div>
);
}
Examples
The use cases for useState are infinite.
Let's start with an easy example.
On/Off Switch
import { useState } from "react";
import { Switch, Text } from "@mantine/core";
function Toggle() {
const [toggled, setToggled] = useState(false);
return (
<Text as="label" size="2">
<Switch checked={toggled} onClick={() => setToggled(!toggled)} />
{toggled ? "On": "Off"}
</Text>
);
}
After defining the state toggled
with an initial value of true
, we show the user a switch.
Each time the switch is clicked, toggle
's value is flipped (true
→false
→true
).
The state is used to conditionally render the correct text "On"
or "Off"
.
Moving to a more complex example, a Todo App!
Todo App
Enter a task and hit ↵
import { useState } from "react";
export type TodoItem = {
text: string;
checked: boolean;
id: string;
};
type TodoProps = {
todo: TodoItem;
onCheckedChange: () => void;
};
export const Todo = ({
todo: { checked, text, id },
onCheckedChange,
}: TodoProps) => {
return (
<div className={"flex"}>
<input
type="checkbox"
id={id}
name="scales"
checked={checked}
onChange={onCheckedChange}
/>
<label htmlFor={id}>{text}</label>
</div>
);
};
export function TodoList() {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [newTodoText, setNewTodoText] = useState("");
function onCheckedChange(id: string) {
setTodos(
todos.map((todo) =>
id === todo.id
? {
...todo,
checked: !todo.checked,
}
: todo,
),
);
}
function addNewTodo() {
const newTodo = {
text: newTodoText,
checked: false,
id: crypto.randomUUID(),
};
setTodos([...todos, newTodo]);
setNewTodoText("");
}
return (
<div>
{todos.map((todo) => (
<Todo
key={todo.id}
todo={todo}
onCheckedChange={() => onCheckedChange(todo.id)}
/>
))}
<input
type={"text"}
placeholder={"Add Todo..."}
value={newTodoText}
onKeyDown={(e) => e.key === "Enter" && addNewTodo()}
onChange={(e) => setNewTodoText(e.target.value)}
/>
<p>
Enter a task and hit <kbd>↵</kbd>
</p>
</div>
);
}
Although a bit more complex, we only use 2 states:
todos
: An array of todo items. Each Todo has an id, a name, and a checked field.newTodoText
: Here we store the user input until submitted.
Pitfalls
While useState
seems simple on the surface, there are a lot of pitfalls you should be aware of.
Setting The State Manually
state = false; // ❌ don't setState(false); // ✅ do
Do not set state manually, always call setState.
The reason, is that setState
doesn't only updates the value.
It also notifies React that component needs to be re-rendered.
Working With Arrays and Objects
Same applies for Arrays, don't mutate the array directly:
// ❌ don't state.add(newElement); // ✅ do setState([...state, newElement]);
[...state, newElement]
create a new Array with all the old state elements, plus the new Element.
This was React knows that there is a new value to show it to the user.
The same logic applies to objects:
// ❌ don't state.name = newName; // ✅ do setState({ ...state, name: newName });
{...state, name: newName}
create a new Object with all the fields from the old state, and overwrites the name.
Calling setState
Subsequently
Mini Quiz: Can you guess what sill be shown when you click on the button?
Current count:0
import { useState } from "react";
export function Component() {
const [count, setCount] = useState(0);
function onButtonClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
return (
<div>
<p>Current count:{count}</p>
<button onClick={onButtonClick}>Add 3 </button>
</div>
);
}
Weird, right?
That's because when you call setState
, React doesn't change the state value directly.
Instead, it re-renders your component with the new state.
A quick fix would be to call setState
with a function that transforms the old value:
function onButtonClick() { setCount((oldCount) => oldCount + 1); setCount((oldCount) => oldCount + 1); setCount((oldCount) => oldCount + 1); }
Tips
Using undefined
When initializing a state with undefined
, you can just leave out the initial value.
const [state, setState] = useState(undefined); const [state, setState] = useState(); // same, but shorter
Single Source Of Truth
When working with different states, avoid duplicating data.
// ❌ don't const [items, setItems] = useState([]); const [selectedItem, setSelectedItem] = useState(); // ✅ do const [items, setItems] = useState([]); const [selectedItemId, setSelectedItemId] = useState(); const selectedItem = items.find((item) => item.id === selectedItemId);
The first approach was duplicating the selected element, you can easily deal with stale data when the items are updated.
The solution is to have the bare minimum state by avoiding duplication.
Typescript Usage
TypeScript is a great way to keep your code type-safe.
useState
works seamlessly: You can pass a type parameter to the function:
const [name, setName] = useState<string>("");
If you try to call setState
with anything but a string, your editor will scream at you!
But when providing an initial value, TypeScript can infer the type from it.
const [name, setName] = useState("");
So most of the time you can leave the explicit types out, while still having all the goodies from TypeScript!
useState
Alternatives
useState
is a powerful hook, but don't over use it!
useReducer and useLocalstorage can also be great alternatives.