The useState hook manages component state:
import { useState } from 'react';
function ToggleButton() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}
useState(false) creates state initialized to false. setIsOn updates it.
Basic Syntax
const [state, setState] = useState(initialValue);
Returns an array with two elements:
-
state: Current state value -
setState: Function to update state
Array destructuring lets you name them anything:
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isOpen, setIsOpen] = useState(false);
Updating State
Direct value:
setCount(5); // Set count to 5
setName('Alice');
setIsOpen(true);
Function update (based on previous state):
setCount(prevCount => prevCount + 1);
setName(prevName => prevName.toUpperCase());
Use function updates when new state depends on old state. This prevents stale state bugs.
State Updates Are Asynchronous
State doesn't update immediately:
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // Still 0 - hasn't updated yet
}
React batches state updates for performance. The component re-renders after the event handler completes.
Multiple Updates in Same Render
Multiple setState calls in the same handler are batched:
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// count increases by 1, not 3
}
All three use the same count value. Use function updates:
function handleClick() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// count increases by 3
}
Initial State
Static value:
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
const [items, setItems] = useState([]);
Lazy initialization (for expensive computation):
const [data, setData] = useState(() => {
return expensiveComputation();
});
The function runs only once on mount. Use this when initial state requires calculation.
State Types
Boolean:
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true);
Number:
const [count, setCount] = useState(0);
const [age, setAge] = useState(25);
String:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
Array:
const [items, setItems] = useState([]);
const [users, setUsers] = useState([{ id: 1, name: 'Alice' }]);
Object:
const [user, setUser] = useState({ name: '', email: '' });
const [form, setForm] = useState({ username: '', password: '' });
Updating Objects and Arrays
State updates must be immutable. Don't mutate:
// Wrong - mutates state
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26; // Don't do this
setUser(user); // React won't detect change
// Correct - create new object
setUser({ ...user, age: 26 });
For arrays:
// Wrong
items.push(newItem);
setItems(items);
// Correct
setItems([...items, newItem]);
// Removing item
setItems(items.filter(item => item.id !== idToRemove));
// Updating item
setItems(items.map(item =>
item.id === targetId ? { ...item, updated: true } : item
));
Form Inputs
Controlled components use useState for input values:
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
function handleSubmit(e) {
e.preventDefault();
console.log({ name, email });
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
/>
<button>Submit</button>
</form>
);
}
Multiple State Variables vs Single Object
Two approaches:
Multiple state variables:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
Single object:
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// Update one field
setUser({ ...user, email: 'new@email.com' });
Use multiple variables for unrelated state. Use objects when state is related and often updated together.
Resetting State
Set back to initial value:
const [count, setCount] = useState(0);
function reset() {
setCount(0);
}
For complex initial state, extract to variable:
const initialState = { name: '', email: '', age: 0 };
const [user, setUser] = useState(initialState);
function reset() {
setUser(initialState);
}
Conditional State Updates
Only update if value changed:
function handleChange(newValue) {
if (newValue !== value) {
setValue(newValue);
}
}
React optimizes this internally, but explicit checks can prevent unnecessary re-renders in child components.
State with useEffect
Common pattern—fetch data and store in state:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Derived State
Don't store values you can calculate:
// Bad - storing derived value
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);
// Update both when adding item
setItems([...items, newItem]);
setItemCount(itemCount + 1);
// Good - calculate on the fly
const [items, setItems] = useState([]);
const itemCount = items.length;
useState vs useReducer
For complex state logic, use useReducer:
// Simple counter - useState is fine
const [count, setCount] = useState(0);
// Complex state with multiple actions - useReducer better
const [state, dispatch] = useReducer(reducer, initialState);
TypeScript with useState
Type inference usually works:
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
Explicit typing when needed:
const [user, setUser] = useState<User | null>(null);
Common Mistakes
Mutating state:
// Wrong
user.name = 'Bob';
setUser(user);
// Correct
setUser({ ...user, name: 'Bob' });
Reading state immediately after setting:
// Wrong
setCount(count + 1);
console.log(count); // Still old value
// Correct - use effect
useEffect(() => {
console.log(count); // Updated value
}, [count]);
Not using function updates with dependencies:
// Problematic if count is stale
setInterval(() => setCount(count + 1), 1000);
// Better
setInterval(() => setCount(c => c + 1), 1000);
Further Reading
React's useState documentation covers all behaviors and examples.
The React docs on state explain when and how to use state in components.
Dan Abramov's guide to useEffect also covers useState patterns.
useState is the foundation of state management in functional React components.
0 comments