React components re-render when their data changes. But regular JavaScript variables do not cause re-renders — when you write let count = 0; count++, the component function runs again on the next render and resets count to 0. State is React’s solution: a special kind of memory that persists between renders and triggers a re-render when updated. The useState hook gives a component state by storing a value in React’s internal memory and providing a setter function that both updates the value and schedules a re-render.
Why Regular Variables Don’t Work
// ❌ Wrong — regular variable resets every render
function Counter() {
let count = 0; // reset to 0 on every render!
return (
<div>
<p>Count: {count}</p>
<button onClick={() => { count++; }}>
Increment
</button>
</div>
);
// Clicking the button increments count, but React never re-renders
// because no state changed — and even if it re-rendered, count would
// be reset to 0 because the function body re-executes from scratch.
}
// ✓ Correct — useState persists between renders
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
// useState(0) returns [currentValue, setterFunction]
// React stores 'count' in its own memory — it survives re-renders
// Calling setCount() updates the stored value AND triggers re-render
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Note: State is local to the component instance. If you render
<Counter /> twice on the same page, each has its own independent count state — clicking one does not affect the other. This is a fundamental property: React components are isolated by default. State is not shared between component instances unless you lift it to a common ancestor or use a global state store.Tip: Think of state as a snapshot: when React renders a component, it captures the current state values as a snapshot. All event handlers in that render see the state values from that snapshot, not the latest state. This is why
setCount(count + 1) called three times in one event handler still only increments by 1 — all three calls see the same snapshot value of count. To increment three times, use the functional form: setCount(prev => prev + 1) three times.Warning: Never call
useState inside loops, conditions, or nested functions. React tracks hooks by their call order — if a hook is called conditionally, the order can change between renders, causing React to map the wrong state to the wrong hook call. The rule is: always call hooks at the top level of a function component, never inside if blocks or for loops. ESLint’s eslint-plugin-react-hooks enforces this rule automatically.State is Asynchronous — The Snapshot Mental Model
function LikeButton({ postId }) {
const [likes, setLikes] = useState(0);
// ❌ Wrong — three setLikes calls use the same snapshot value
function handleTripleLike() {
setLikes(likes + 1); // schedules: set to 0+1 = 1
setLikes(likes + 1); // schedules: set to 0+1 = 1 (same snapshot!)
setLikes(likes + 1); // schedules: set to 0+1 = 1 (same snapshot!)
// Result: likes = 1, not 3!
}
// ✓ Correct — functional form always uses the latest pending value
function handleTripleLike() {
setLikes((prev) => prev + 1); // schedules: 0 → 1
setLikes((prev) => prev + 1); // schedules: 1 → 2
setLikes((prev) => prev + 1); // schedules: 2 → 3
// Result: likes = 3 ✓
}
return (
<button onClick={handleTripleLike}>
♥ {likes}
</button>
);
}
When to Use State vs Derived Values
function PostSearch({ posts }) {
const [searchQuery, setSearchQuery] = useState("");
// ✓ State: user input that triggers re-render when changed
// ✓ Derived value — computed from state, NOT separate state
const filteredPosts = posts.filter((p) =>
p.title.toLowerCase().includes(searchQuery.toLowerCase())
);
// DON'T do: const [filteredPosts, setFilteredPosts] = useState([]);
// Then keep it in sync with searchQuery — that's redundant state.
// Just compute it during render from the source of truth (searchQuery).
return (
<div>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search posts..."
/>
<PostList posts={filteredPosts} />
</div>
);
}
Common Mistakes
Mistake 1 — Calling useState conditionally
❌ Wrong — breaks React’s hook order rules:
function Component({ isLoggedIn }) {
if (isLoggedIn) {
const [name, setName] = useState(""); // NEVER inside if!
}
}
✅ Correct — always at the top level:
function Component({ isLoggedIn }) {
const [name, setName] = useState(""); // ✓ always called
if (!isLoggedIn) return null;
}
Mistake 2 — Using state for values that can be derived
❌ Wrong — redundant state that must be kept in sync:
const [posts, setPosts] = useState([]);
const [publishedCount, setPublishedCount] = useState(0); // redundant!
// Must update publishedCount every time posts changes
✅ Correct — derive it:
const [posts, setPosts] = useState([]);
const publishedCount = posts.filter(p => p.status === "published").length; // ✓
Quick Reference
| Task | Code |
|---|---|
| Declare state | const [value, setValue] = useState(initialValue) |
| Update state | setValue(newValue) |
| Update based on prev | setValue(prev => prev + 1) |
| Boolean toggle | setIsOpen(prev => !prev) |
| Initial value (lazy) | useState(() => expensiveComputation()) |
| Derived value | Compute in render body — no extra useState needed |