useState & State Updates
EasyuseState is the fundamental hook for adding local state to functional components. It returns a state value and a setter function. State updates are asynchronous and batched, and using the updater function form ensures correct updates when the new value depends on the previous one.
Interactive Visualization
Key Points
- Returns a [value, setter] tuple, initialized with the argument
- Setter triggers a re-render with the new value
- Use updater function form when new state depends on previous: setValue(prev => prev + 1)
- State updates are batched in React 18+ for all event types
- Lazy initialization accepts a function to avoid expensive computation on every render
- State identity check uses Object.is() — same value skips re-render
- Objects and arrays must be replaced, not mutated, to trigger re-renders
Code Examples
Counter with Updater Function
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) const increment = () => { // Updater form: correct when called multiple times setCount((prev) => prev + 1) } const incrementThrice = () => { // All three updates use the latest value setCount((prev) => prev + 1) setCount((prev) => prev + 1) setCount((prev) => prev + 1) // count will increase by 3 } return ( <div> <p>Count: {count}</p> <button onClick={increment}>+1</button> <button onClick={incrementThrice}>+3</button> </div> ) }
The updater function receives the latest pending state, ensuring correct results when multiple updates are batched
Object State with Immutable Updates
import { useState } from 'react' interface FormData { name: string email: string age: number } function ProfileForm() { const [form, setForm] = useState<FormData>({ name: '', email: '', age: 0, }) const updateField = <K extends keyof FormData>( field: K, value: FormData[K] ) => { setForm((prev) => ({ ...prev, [field]: value })) } return ( <form> <input value={form.name} onChange={(e) => updateField('name', e.target.value)} /> <input value={form.email} onChange={(e) => updateField('email', e.target.value)} /> </form> ) }
Object state must be replaced with a new object using spread syntax, never mutated directly
Lazy Initialization
import { useState } from 'react' function ExpensiveComponent() { // Function is only called on initial render const [data, setData] = useState(() => { const stored = localStorage.getItem('app-data') return stored ? JSON.parse(stored) as Record<string, unknown> : {} }) return <div>{JSON.stringify(data)}</div> }
Passing a function to useState defers expensive computation to the first render only, avoiding it on every re-render
Common Mistakes
- Mutating state objects directly instead of creating new references
- Using setCount(count + 1) inside loops or async code instead of the updater function
- Expecting state to update synchronously right after calling the setter
Interview Tips
- Explain batching: React 18 batches all state updates, not just event handlers
- Know why Object.is() comparison means you must create new references for objects
- Discuss when to use useState vs useReducer for complex state