Why Not Just Use a Regular Variable?
A regular variable inside a component resets every time the component re-renders, and changing it doesn’t trigger a re-render at all. State solves both problems: it persists between renders, and updating it tells React to re-render the component.
A Basic Counter with useState
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}Reading the useState Return Value
useState(initialValue) returns an array with exactly two items: the current value, and a function to update it. Array destructuring is used to name them however you like.
useState Return Value
| Item | Purpose |
|---|---|
| count | The current state value for this render |
| setCount | A function that updates the state and schedules a re-render |
State Updates Trigger Re-Renders
Calling the setter function tells React: "the state changed, please re-render this component (and its children) with the new value." The component function runs again from the top, returning new JSX based on the updated state.
Functional Updates
When a new state value depends on the previous one, pass a function to the setter instead of a plain value — this guarantees you’re always updating from the latest state, even if multiple updates are queued together.
Functional Update Form
function Counter() {
const [count, setCount] = useState(0);
function handleTripleIncrement() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}
return <button onClick={handleTripleIncrement}>Count: {count}</button>;
// Correctly increments by 3, since each update reads the latest prev value
}State Updates Are Asynchronous (Batched)
setCount(count + 1) doesn’t update count immediately — it schedules an update for the next render. Reading count right after calling the setter still shows the old value, which is why the functional update form matters for sequential updates.
Never Mutate State Directly
State must always be updated by calling its setter with a new value — mutating an object or array in place and expecting a re-render will not work, since React compares references to detect changes.
Updating Object State Immutably
function Profile() {
const [user, setUser] = useState({ name: "Alice", age: 30 });
function birthday() {
// Wrong: user.age++; setUser(user); — mutates in place, won't trigger a re-render
setUser({ ...user, age: user.age + 1 }); // Correct: a new object
}
return <button onClick={birthday}>{user.name} is {user.age}</button>;
}Best Practice
Keep state as minimal as possible — don’t store anything in state that can be derived from other state or props during render. Fewer state variables means fewer places for state to get out of sync.