Introduction
useState is the most commonly used React Hook. It lets a function component declare a piece of local state that persists across re-renders. Calling useState returns an array with two elements: the current state value and a function to update it. When the updater function is called, React re-renders the component with the new state value, similar to how this.setState works in class components but scoped to a single piece of state.
Cricket analogy: useState is like a single entry on the scoreboard for 'current batsman's score' — calling the update function is like the scorer signaling a new run tally, which redraws just that scoreboard segment, unlike the old class-component style of updating the whole board object at once.
Syntax
const [state, setState] = useState(initialValue);
// Example
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}Explanation
The argument passed to useState becomes the state's initial value on the first render only; on subsequent renders, that argument is ignored and React returns the current stored value. If computing the initial value is expensive, you can pass a function instead (lazy initialization), e.g. useState(() => expensiveComputation()), and React will call it only once. The setter function can accept either a new value directly, or an updater function of the form (prevState) => newState. The functional form is important when the new state depends on the previous state, especially inside closures like event handlers, timers, or effects, because it always receives the most current state rather than a possibly stale value captured by the closure. React also batches multiple setState calls that occur within the same event handler into a single re-render for performance. Unlike this.setState in classes, useState's setter replaces the state entirely rather than merging it, so when state is an object you must spread the previous object manually to keep unrelated fields.
Cricket analogy: The opening pitch report is only read once at the start of the innings — recalculating it every over would be wasteful, so a lazy initializer computes it once, like a groundskeeper's expensive pitch analysis done only at toss time. Using the functional updater form when adding runs from three quick boundaries in one over ensures each addition reads the true current total rather than a stale total remembered from the over's start, and because useState replaces rather than merges, you must carry forward the wickets and overs fields manually when updating just the runs.
Example
function ProfileForm() {
const [user, setUser] = useState({ name: '', age: 0 });
function updateName(newName) {
// Must spread previous state since setState replaces, not merges
setUser(prev => ({ ...prev, name: newName }));
}
function incrementAgeThreeTimes() {
// Functional updates guarantee correctness even when called multiple times
setUser(prev => ({ ...prev, age: prev.age + 1 }));
setUser(prev => ({ ...prev, age: prev.age + 1 }));
setUser(prev => ({ ...prev, age: prev.age + 1 }));
}
return (
<div>
<input value={user.name} onChange={e => updateName(e.target.value)} />
<p>Age: {user.age}</p>
<button onClick={incrementAgeThreeTimes}>+3 years</button>
</div>
);
}Output
Typing into the input updates only the name field while preserving age, because the functional updater spreads the previous state. Clicking '+3 years' correctly increases age by exactly 3, because each functional update reads the latest pending state rather than a stale snapshot captured when incrementAgeThreeTimes was defined. If a plain value form (setUser({...user, age: user.age + 1})) were called three times in a row, all three calls would reference the same stale 'user' from the closure and age would only increase by 1.
Cricket analogy: Editing a player's nickname on the roster updates only that field while their runs-scored tally stays intact, because the update spreads the previous player object. Tapping '+3 overs bowled' three times correctly adds 3 total because each functional update reads the latest pending count; if a plain value form referencing the same stale 'overs' variable were used three times, the tally would only increase by 1.
Key Takeaways
- useState(initialValue) returns [currentValue, setterFunction] and is used for local component state.
- The initial value argument is only used on the very first render.
- Use lazy initialization (useState(() => compute())) for expensive initial state computations.
- The setter can take a new value or an updater function (prevState) => newState; prefer the functional form when new state depends on old state.
- useState replaces state rather than merging it, so object/array state must be manually spread to preserve unrelated fields.
- Multiple setState calls in the same event handler are batched into a single re-render.
Practice what you learned
1. What does calling useState(0) return?
2. Why is setCount(prev => prev + 1) generally safer than setCount(count + 1) inside a rapidly-called handler?
3. If state is an object like { name: 'A', age: 1 } and you call setState({ age: 2 }), what happens?
4. When is the argument passed to useState actually used as the initial value?
Was this page helpful?
You May Also Like
Introduction to React Hooks
Learn what React Hooks are, why they were introduced in React 16.8, and how they let function components use state and lifecycle features.
The useEffect Hook
Understand useEffect for side effects in function components, covering the dependency array, cleanup functions, and effect timing.
State in React
Understand how state gives components memory, enabling dynamic, interactive UIs that update over time.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics