useState Hook
Learn how to manage local component state with useState
What you'll learn
Initialize and update state with the useState hook
Understand batching and when state updates are applied
Use the functional updater pattern for derived state
Lift state up to share it between sibling components
Manage complex state objects with useState vs useReducer
Avoid common pitfalls: stale closures, missing dependencies
The useState hook lets you add state to functional components.
What is useState?
useState is a built-in React hook that lets you add state to functional components.
import { useState } from "react"
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Basic Usage
Initial State
You can initialize state with a default value:
function NameInput() {
const [name, setName] = useState("Guest")
return <input value={name} onChange={(e) => setName(e.target.value)} />
}
Updating State
There are two ways to update state:
- Directly (not recommended for derived state):
function Counter() {
const [count, setCount] = useState(0)
// ❌ Wrong: don't do this
const increment = () => {
setCount(count + 1) // Will always add 1
}
}
- With Function (recommended):
function Counter() {
const [count, setCount] = useState(0)
// ✅ Correct: use function form
const increment = () => {
setCount((prevCount) => prevCount + 1)
}
return <button onClick={increment}>Count: {count}</button>
}
Multiple State Variables
You can use multiple useState hooks:
function Form() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [age, setAge] = useState(0)
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Age"
/>
</form>
)
}
State Updates Are Batched
React batches state updates for performance:
function Example() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(1)
setCount(2)
setCount(3)
console.log(count) // Still 0!
}
return <button onClick={handleClick}>Click me</button>
}
To fix this, use the function form:
const handleClick = () => {
setCount((prevCount) => {
const newState = prevCount + 1
setCount((prevCount) => {
const newState2 = prevCount + 1
setCount((prevCount) => {
return prevCount + 1 // Now prints 3
})
return newState2
})
return newState
})
}
Derived State vs Local State
// ❌ Wrong: storing derived state
function Person({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`
return <div>{fullName}</div>
}
// ✅ Correct: compute on the fly
function Person({ firstName, lastName }) {
return (
<div>
{firstName} {lastName}
</div>
)
}
Storing Objects and Arrays
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Build projects", completed: false },
])
const toggleTodo = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
)
}
Common Patterns
Resetting State
function Form() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const resetForm = () => {
setName("")
setEmail("")
}
return (
<>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<button onClick={resetForm}>Reset</button>
</>
)
}
Preserving State Across Re-renders
When you delete an item from a list, React reuses the array index. To preserve state, use stable IDs:
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Task 1" },
{ id: 2, text: "Task 2" },
{ id: 3, text: "Task 3" },
])
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id))
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
)
}
Exercise
Create a counter component that:
- Has a count state initialized to 0
- Has an increment button (+1)
- Has a decrement button (-1)
- Has a reset button (to 0)
- Shows the current count
Hint: You'll need 3 state variables or use a single state with a different value format.
Next Lesson
- useEffect - Handle side effects
- useEffect alternative: useReducer
Key Takeaways
useState returns a [value, setter] tuple — the setter triggers a re-render when called with a new value
React batches state updates in event handlers and effects, processing them in a single render
The functional updater (setCount(prev => prev + 1)) is essential when the new state depends on the previous state
Lifting state up shares data between siblings by moving state to the closest common ancestor
For complex state shapes with multiple fields, consider splitting into multiple useState calls or switching to useReducer