React useState Hook
1. useState Syntax and State Declaration (React.useState)
| Syntax | Description | Return Value | Use Case |
|---|---|---|---|
| useState(initialValue) | Declares state variable with initial value | [state, setState] array |
Basic state management for primitives and objects |
| const [value, setValue] | Array destructuring to extract state and setter | State variable and updater function | Naming convention: [noun, setNoun] |
| Multiple useState calls | Each call creates independent state variable | Separate state slices with own setters | Split state by concern for better organization |
| State Type | Declaration | Initial Value | TypeScript Type |
|---|---|---|---|
| Primitive | useState(0) |
Number, string, boolean, null | const [count, setCount] = useState<number>(0) |
| Object | useState({name: ''}) |
Object literal | useState<User>({name: '', age: 0}) |
| Array | useState([]) |
Empty or populated array | useState<string[]>([]) |
| Nullable | useState(null) |
null or undefined |
useState<User | null>(null) |
Example: Basic useState declarations
import { useState } from 'react';
function Counter() {
// Primitive state
const [count, setCount] = useState(0);
// Object state
const [user, setUser] = useState({ name: 'John', age: 30 });
// Array state
const [items, setItems] = useState(['a', 'b', 'c']);
// Boolean state
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
2. Functional State Updates (Updater Functions)
| Pattern | Syntax | When to Use | Advantage |
|---|---|---|---|
| Direct Update | setState(newValue) |
New value doesn't depend on previous state | Simple, straightforward |
| Functional Update RECOMMENDED | setState(prev => newValue) |
New value depends on previous state | Prevents stale closure issues, guarantees latest state |
| Updater with Logic | setState(prev => prev + delta) |
Calculations based on current state | Safe for async updates and batching |
| Scenario | ❌ Problematic | ✅ Correct |
|---|---|---|
| Multiple Updates | setCount(count + 1); |
setCount(c => c + 1); |
| Event Handler | onClick={() => setCount(count + 1)} |
onClick={() => setCount(c => c + 1)} |
| Async Callback | setTimeout(() => setCount(count + 1), 1000) |
setTimeout(() => setCount(c => c + 1), 1000) |
Example: Functional updates prevent stale closures
// ❌ Problem: Both clicks only increment once total
function BadCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // Uses stale 'count'
setCount(count + 1); // Uses same stale 'count'
};
return <button onClick={handleClick}>{count}</button>;
}
// ✅ Solution: Functional updates use latest state
function GoodCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(c => c + 1); // Uses latest state
setCount(c => c + 1); // Uses result of previous update
};
return <button onClick={handleClick}>{count}</button>; // Increments by 2
}
Best Practice: Always use functional updates when the new state depends on the previous state,
especially in event handlers, async operations, and when multiple updates may be batched.
3. Lazy State Initialization with Functions
| Pattern | Syntax | Execution | Performance Impact |
|---|---|---|---|
| Direct Initialization | useState(expensiveCalc()) |
Runs on every render | ❌ Wasteful - recalculates unnecessarily |
| Lazy Initialization RECOMMENDED | useState(() => expensiveCalc()) |
Runs only once on mount | ✅ Optimized - calculates only when needed |
| Lazy with Dependencies | useState(() => computeFromProps(props)) |
Once on mount with prop values | ✅ Efficient initial state from props |
| Use Case | Example | Why Lazy Init |
|---|---|---|
| localStorage Read | useState(() => JSON.parse(localStorage.getItem('key'))) |
Avoid reading storage on every render |
| Complex Calculation | useState(() => expensiveDataProcessing(data)) |
Defer expensive computation to mount only |
| Date/Time | useState(() => new Date()) |
Capture exact mount time, not re-render time |
| Random Value | useState(() => Math.random()) |
Ensure consistent initial random value |
Example: Lazy initialization for expensive operations
// ❌ Bad: Reads localStorage on EVERY render (wasteful)
function BadComponent() {
const [data, setData] = useState(
JSON.parse(localStorage.getItem('userData') || '{}')
);
// This executes on every render, even though result is ignored after mount
}
// ✅ Good: Reads localStorage ONLY on initial mount
function GoodComponent() {
const [data, setData] = useState(() => {
const saved = localStorage.getItem('userData');
return saved ? JSON.parse(saved) : {};
});
// Function executes once, subsequent renders skip it
}
// ✅ Good: Expensive calculation done only once
function DataProcessor({ rawData }) {
const [processed, setProcessed] = useState(() => {
console.log('Processing...'); // Logs only once
return expensiveProcessing(rawData);
});
}
Warning: Lazy initializer function runs only once on mount. It won't re-run if props change.
For derived state from props, use
useMemo or useEffect instead.
4. State Update Batching (React 18 Automatic Batching)
| React Version | Batching Scope | Behavior |
|---|---|---|
| React 17 | Event handlers only | Batches updates in synthetic events, but not in setTimeout, promises, native events |
| React 18+ NEW | All updates everywhere | Automatic batching in timeouts, promises, native events, and event handlers |
| Context | React 17 Batching | React 18 Batching | Re-renders |
|---|---|---|---|
| Event Handler | ✅ Batched | ✅ Batched | 1 re-render for multiple setState calls |
| setTimeout | ❌ Not batched | ✅ Batched | React 18: 1 re-render; React 17: multiple |
| Promise.then | ❌ Not batched | ✅ Batched | React 18: 1 re-render; React 17: multiple |
| Native Event | ❌ Not batched | ✅ Batched | React 18: 1 re-render; React 17: multiple |
| API | Purpose | Usage |
|---|---|---|
| flushSync() | Force immediate synchronous update, opt-out of batching | flushSync(() => setState(value)) |
| ReactDOM.unstable_batchedUpdates() | Manual batching in React 17 (deprecated in React 18) | unstable_batchedUpdates(() => { setState1(); setState2(); }) |
Example: Automatic batching in React 18
import { useState } from 'react';
import { flushSync } from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Render'); // Track re-renders
// React 18: Both updates batched → 1 render
// React 17: Both updates batched → 1 render
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// Single re-render after both updates
}
// React 18: Both updates batched → 1 render
// React 17: Each update causes render → 2 renders
function handleAsync() {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18: Single re-render
}, 1000);
}
// React 18: Both updates batched → 1 render
// React 17: Each update causes render → 2 renders
function handlePromise() {
fetch('/api').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18: Single re-render
});
}
// Opt-out: Force immediate synchronous updates
function handleFlushSync() {
flushSync(() => {
setCount(c => c + 1); // Renders immediately
});
flushSync(() => {
setFlag(f => !f); // Renders immediately (separate)
});
// Two separate re-renders
}
}
React 18 Benefit: Automatic batching improves performance by reducing re-renders. Multiple
state updates in any context are batched into a single re-render, leading to better performance and more
consistent behavior.
5. Complex State Objects and State Merging
| Pattern | Behavior | Syntax | Note |
|---|---|---|---|
| useState (Replaces) | Replaces entire state object | setState(newObject) |
❌ No automatic merging - must spread manually |
| Class setState (Merges) | Merges with existing state | this.setState({key: value}) |
⚠️ Legacy - not available in hooks |
| Manual Merge | Spread existing state + new values | setState(prev => ({...prev, key: value})) |
✅ Recommended pattern for object state |
| Update Pattern | Code | Result |
|---|---|---|
| Single Property | setUser(prev => ({...prev, name: 'Jane'})) |
Updates name, preserves other properties |
| Multiple Properties | setUser(prev => ({...prev, name: 'Jane', age: 25})) |
Updates multiple fields at once |
| Nested Object | setUser(prev => ({...prev, address: {...prev.address, city: 'NYC'}})) |
Update nested property immutably |
| Computed Property | setUser(prev => ({...prev, [key]: value})) |
Dynamic key based on variable |
Example: Object state updates and merging
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
address: { city: '', country: '' }
});
// ❌ Wrong: Loses all other properties
const updateNameBad = (name) => {
setUser({ name }); // user now only has 'name', lost email, age, address
};
// ✅ Correct: Spreads existing state, updates one field
const updateName = (name) => {
setUser(prev => ({ ...prev, name }));
};
// ✅ Multiple fields at once
const updateMultiple = () => {
setUser(prev => ({
...prev,
name: 'John',
age: 30
}));
};
// ✅ Nested object update
const updateCity = (city) => {
setUser(prev => ({
...prev,
address: { ...prev.address, city }
}));
};
// ✅ Dynamic property name
const updateField = (field, value) => {
setUser(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input value={user.name} onChange={e => updateName(e.target.value)} />
<input value={user.email} onChange={e => updateField('email', e.target.value)} />
</form>
);
}
Warning:
useState does NOT merge objects automatically like class component
setState. Always use the spread operator {...prev, ...changes} to preserve unmodified
properties.
6. State Reset Patterns and Key Changes
| Reset Method | Implementation | Use Case |
|---|---|---|
| setState to Initial | setState(initialValue) |
Simple reset to known initial state |
| Key Prop Change RECOMMENDED | <Component key={id} /> |
Force complete component remount with fresh state |
| useEffect Reset | useEffect(() => setState(initial), [dependency]) |
Reset state when dependency changes |
| Reset Function | const reset = () => setState(INITIAL_STATE) |
Reusable reset handler |
| Pattern | Code Example | Behavior |
|---|---|---|
| Constant Initial State | const INITIAL = { name: '', age: 0 }; |
Reference constant for resets: setState(INITIAL) |
| Factory Function | const createInitial = () => ({name: '', age: 0}); |
Reset with: setState(createInitial()) |
| Key-based Reset | <Form key={userId} /> |
Changing userId unmounts and remounts Form with fresh state |
Example: State reset patterns
const INITIAL_FORM = { name: '', email: '', age: 0 };
function Form() {
const [form, setForm] = useState(INITIAL_FORM);
// Method 1: Direct reset to constant
const handleReset = () => {
setForm(INITIAL_FORM);
};
// Method 2: Factory function for reset
const createInitialState = () => ({
name: '',
email: '',
age: 0,
timestamp: Date.now() // Fresh timestamp on each reset
});
const [data, setData] = useState(createInitialState);
const handleResetWithFactory = () => {
setData(createInitialState());
};
return (
<form>
<input value={form.name} onChange={e => setForm(prev => ({...prev, name: e.target.value}))} />
<button type="button" onClick={handleReset}>Reset</button>
</form>
);
}
// Method 3: Key-based reset (component remounts)
function UserProfile({ userId }) {
return <ProfileForm key={userId} />; // New userId = fresh state
}
function ProfileForm() {
const [name, setName] = useState(''); // Resets when key changes
const [email, setEmail] = useState('');
// State automatically resets when parent's key prop changes
}
Example: Reset state when prop changes
// Pattern: Reset internal state when prop changes
function EditableItem({ item }) {
const [localValue, setLocalValue] = useState(item.value);
// Reset local state when item prop changes
useEffect(() => {
setLocalValue(item.value);
}, [item.value]);
return (
<input
value={localValue}
onChange={e => setLocalValue(e.target.value)}
/>
);
}
// Alternative: Use key to force remount
function ItemList({ items }) {
return items.map(item => (
<EditableItem key={item.id} item={item} />
));
}
useState Best Practices Summary
- Use functional updates when new state depends on previous state
- Apply lazy initialization for expensive initial calculations
- Leverage automatic batching in React 18+ for better performance
- Always spread objects when updating - useState doesn't merge
- Use key prop to reset component state completely
- Store initial state in constants for easy resets