useState
is a React Hook that lets you add state to functional components. It returns an array with two elements: the current state value and a function to update it.
const [state, setState] = useState(initialValue);
// Examples:
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
const [items, setItems] = useState([]);
const [user, setUser] = useState({ name: 'Alice', age: 30 });
✅ Uses functional update: setCount(prev => prev + 1)
✅ Uses functional update: setIsToggled(prev => !prev)
❌ items.push(newItem)
✅ setItems(prev => [...prev, newItem])
Name: John
Age: 25
❌ user.name = newName
✅ setUser(prev => ({ ...prev, name: newName }))
setCount(prev => prev + 1)
vs setCount(count + 1)
Why functional updates? They ensure you're working with the latest state value, especially important in async operations, event handlers, and when multiple updates happen quickly. The functional approach prevents stale closures.
[...prev, newItem]
| Objects: { ...prev, newProp: value }
Why immutability? React uses Object.is() comparison to detect state changes. If you mutate the original object/array, React won't detect the change and won't re-render. Always create new references for updates.
setTimeout(() => setCount(prev => prev + 1), 1000)
The problem: In closures (like setTimeout, useEffect), the count
variable captures the value from when the closure was created, not the current value. Using functional updatesprev => prev + 1
ensures you get the latest state.
setState
calls in one handler = one re-renderHow it works: React automatically batches multiple state updates in event handlers into a single re-render for better performance. In React 18+, this batching also happens in promises, timeouts, and other async operations (Automatic Batching).
setCount(count + 1); console.log(count); // Still old value!
Remember: State updates are scheduled and happen after the current execution. If you need to perform actions after state updates, use useEffect or access the new state in the next render cycle.