Calling useState(initialValue) returns an array with exactly two elements: the current state value and a function to update it, conventionally unpacked via array destructuring as [value, setValue]. The initialValue argument is only used on the very first render; on every subsequent render, useState returns whatever the current state actually is, ignoring the initial value passed in. Calling the setter schedules a state update and a subsequent re-render, but note the update isn't applied synchronously — the state variable inside the currently-executing function body still reflects the old value immediately after calling the setter, only the next render sees the new one.
1Understanding useState
Calling useState(initialValue) returns an array with exactly two elements: the current state value and a function to update it, conventionally unpacked via array destructuring as [value, setValue]. The initialValue argument is only used on the very first render; on every subsequent render, useState returns whatever the current state actually is, ignoring the initial value passed in. Calling the setter schedules a state update and a subsequent re-render, but note the update isn't applied synchronously — the state variable inside the currently-executing function body still reflects the old value immediately after calling the setter, only the next render sees the new one.
Pass a function to useState, useState(() => expensiveComputation()), rather than calling that expensive computation directly, useState(expensiveComputation()), when the initial value is costly to compute — the function form only ever runs once, on the first render, while the direct form re-runs the expensive computation on every single render even though its result is discarded after the first.
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}2Practical Example
Here is a real-world application of useState showing how it is used in production React code.
import { useState } from 'react';
function ExpensiveInit() {
const [value] = useState(() => {
console.log('Computing initial value...');
return 42;
});
return <p>{value}</p>;
}3Best Practices
Follow these guidelines when working with useState:
1. Pass a function, not a direct value, to useState when the initial value requires an expensive computation, so it only runs once on mount
2. Use the functional updater form, setValue(prev => ...), whenever a new value depends on the current one, to avoid stale-value bugs
3. Split unrelated pieces of state into separate useState calls rather than combining everything into one large state object, for simpler, more targeted updates
Tip: Pass a function to useState, useState(() => expensiveComputation()), rather than calling that expensive computation directly, useState(expensiveComputation()), when the initial value is costly to compute — the function form only ever runs once, on the first render, while the direct form re-runs the expensive computation on every single render even though its result is discarded after the first.
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}4The Functional Updater Form
When a new value depends on the previous one, pass a function to the setter instead of a direct value: setCount(prev => prev + 1). React guarantees this updater always receives the most current pending state, so it stays correct even when several updates are queued in the same event — calling setCount(count + 1) multiple times in a row reuses the same stale count each time, but the updater form correctly accumulates.
// Unsafe with multiple calls
setCount(count + 1);
setCount(count + 1); // still uses old count
// Safe: always uses the latest pending value
setCount(prev => prev + 1);
setCount(prev => prev + 1);5Automatic Batching
React 18 batches every state update triggered inside the same event handler, promise callback, or timeout into a single re-render, no matter how many separate setter calls are made. This is a performance optimization that generally requires no extra code from you, but it explains why logging state right after calling a setter, several times in a row, always shows the same (stale) value within that render.
function handleClick() {
setA(1);
setB(2);
setC(3);
// Still only ONE re-render
}6Immutability with Objects and Arrays
React detects a state change by comparing the new value to the old one with Object.is, which checks objects and arrays by reference. Mutating an object or array in place — pushing to an array, or assigning a property directly — leaves the reference unchanged, so React never sees a difference and skips the re-render. Always create a new object or array and pass that to the setter.
This is one of the most common React bugs: mutating a state object or array directly, then calling the setter with that same, now-mutated reference — React still bails out of the re-render because Object.is sees no change.
// Wrong: mutates in place, same reference
user.name = 'Bob';
setUser(user); // React sees no change
// Correct: new object, new reference
setUser({ ...user, name: 'Bob' });