React useState Hook — Complete Guide
Learn React useState from basics to advanced patterns. Covers state initialization, functional updates, objects in state, and common mistakes to avoid.
React useState Hook — Complete Guide
useState is the most fundamental React hook. It lets you add state to functional components — a value that persists across renders and triggers re-renders when updated.
Basic Usage
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useState(0) returns a pair:
count— the current state valuesetCount— a function to update the state
When you call setCount, React re-renders the component with the new value.
Functional Updates
When the new state depends on the previous state, use a function:
// Risky — might use stale value
setCount(count + 1);
// Safe — always uses latest value
setCount(prevCount => prevCount + 1);
Why does this matter? If you call setCount multiple times in the same event handler, React batches updates. Without functional updates, you'd be using the same stale count value:
function handleTripleIncrement() {
// Wrong — all three use the same `count` value
setCount(count + 1);
setCount(count + 1);
setCount(count + 1); // Only increments by 1!
// Correct — each uses the latest value
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1); // Increments by 3 ✓
}
Objects in State
When state is an object, you must create a new object (not mutate the existing one):
const [user, setUser] = useState({ name: "Rahul", age: 22 });
// Wrong — mutating state directly
user.name = "Priya";
setUser(user); // React won't re-render!
// Correct — create a new object
setUser({ ...user, name: "Priya" });
React uses reference equality to detect changes. If you pass the same object reference, React thinks nothing changed.
Nested Objects
const [form, setForm] = useState({
personal: { name: "", email: "" },
address: { city: "", state: "" }
});
// Update nested field
setForm(prev => ({
...prev,
personal: { ...prev.personal, name: "Amit" }
}));
For deeply nested state, consider using useReducer instead.
Arrays in State
const [items, setItems] = useState(["Apple", "Banana"]);
// Add item
setItems([...items, "Cherry"]);
// Remove item
setItems(items.filter((_, index) => index !== 1));
// Update item
setItems(items.map((item, i) => i === 0 ? "Mango" : item));
Never use push, splice, or direct index assignment — these mutate the array.
Lazy Initialization
If computing the initial value is expensive, pass a function:
// Runs every render (wasteful)
const [data, setData] = useState(expensiveComputation());
// Runs only on first render (efficient)
const [data, setData] = useState(() => expensiveComputation());
Common use case — reading from localStorage:
const [theme, setTheme] = useState(() => {
return localStorage.getItem("theme") || "dark";
});
Multiple State Variables vs One Object
Multiple variables (preferred for unrelated state):
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [isStudent, setIsStudent] = useState(true);
Single object (when fields are related):
const [form, setForm] = useState({
name: "",
email: "",
password: ""
});
Rule of thumb: If fields change together, group them. If they change independently, separate them.
Common Mistakes
1. Forgetting State Updates Are Async
State doesn't update immediately after calling the setter:
setCount(5);
console.log(count); // Still the old value!
If you need the new value, use it in the next render or in a useEffect.
2. Setting State in a Loop Without Functional Updates
// Bug — only adds 1
for (let i = 0; i < 5; i++) {
setCount(count + 1);
}
// Correct — adds 5
for (let i = 0; i < 5; i++) {
setCount(c => c + 1);
}
3. Storing Derived Values in State
// Unnecessary — don't store computed values
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // ← redundant
// Just compute it
const count = items.length; // ← derived from items
When to Use useState vs useReducer
| Use useState | Use useReducer |
|---|---|
| 1-3 simple values | Complex state logic |
| Independent values | Values that depend on each other |
| Simple updates | Multiple actions modify state differently |
Key Takeaways
useStatereturns[value, setter]— call the setter to trigger re-render- Use functional updates (
setX(prev => ...)) when new state depends on old state - Never mutate state directly — always create new objects/arrays
- Use lazy initialization for expensive computations
- Don't store derived values in state — compute them instead
🚀 Practice What You Learned
Apply these concepts with hands-on coding challenges: