Why useState?
useState is the most fundamental React hook. It allows functional components to have state - making them interactive and dynamic. Understanding useState deeply is crucial for React mastery.
What You'll Learn:
- Lazy initialization for expensive computations
- Functional updates to avoid stale state
- Multiple state variables vs single object
- State batching and performance
- Common patterns and best practices
📚 Basic Syntax Review
import { useState } from 'react';
function Counter() {
// [currentValue, updateFunction] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
⚡ Lazy Initialization
When initial state requires expensive computation, use lazy initialization:
// ❌ Bad - runs expensive calculation every render
function Component() {
const [data, setData] = useState(expensiveCalculation());
// expensiveCalculation() runs on EVERY render!
}
// ✅ Good - calculation runs only once
function Component() {
const [data, setData] = useState(() => expensiveCalculation());
// Arrow function ensures it only runs on initial render
}
// Real example
function TodoList() {
// Only parse localStorage once on mount
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
return <div>{/* render todos */}</div>;
}
When to Use Lazy Init:
- Reading from localStorage/sessionStorage
- Complex calculations (array processing)
- Date/time operations
- Any operation that takes time
🔄 Functional Updates
Avoid stale state by using the functional update form:
function Counter() {
const [count, setCount] = useState(0);
// ❌ Problem - can miss updates
const handleClick = () => {
setCount(count + 1);
setCount(count + 1); // Still uses old count!
// Result: count increases by 1, not 2
};
// ✅ Solution - functional update
const handleClickCorrect = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
// Result: count increases by 2
};
return (
<div>
<p>{count}</p>
<button onClick={handleClickCorrect}>+2</button>
</div>
);
}
When Functional Updates Matter:
// Multiple updates in quick succession
function AsyncCounter() {
const [count, setCount] = useState(0);
const handleMultipleUpdates = () => {
// These all execute with same count value
setTimeout(() => setCount(count + 1), 1000);
setTimeout(() => setCount(count + 1), 2000);
setTimeout(() => setCount(count + 1), 3000);
// Result: count will be 1, not 3!
};
const handleMultipleUpdatesCorrect = () => {
// Each gets previous state
setTimeout(() => setCount(prev => prev + 1), 1000);
setTimeout(() => setCount(prev => prev + 1), 2000);
setTimeout(() => setCount(prev => prev + 1), 3000);
// Result: count will be 3 ✓
};
return (
<button onClick={handleMultipleUpdatesCorrect}>
Increment 3 times: {count}
</button>
);
}
🎨 Multiple State Variables
Approach 1: Separate State Variables
function UserProfile() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [isActive, setIsActive] = useState(false);
// Pros: Simple, clear updates
// Cons: More code, hard to reset all
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="number" value={age} onChange={e => setAge(+e.target.value)} />
<input type="checkbox" checked={isActive} onChange={e => setIsActive(e.target.checked)} />
</div>
);
}
Approach 2: Single State Object
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
isActive: false
});
// Update single field (must spread existing state!)
const updateField = (field, value) => {
setUser(prevUser => ({
...prevUser, // Keep existing fields
[field]: value // Update specific field
}));
};
// Reset all at once
const reset = () => {
setUser({ name: '', email: '', age: 0, isActive: false });
};
// Pros: Easy to reset, related data grouped
// Cons: Must remember to spread, more complex
return (
<div>
<input
value={user.name}
onChange={e => updateField('name', e.target.value)}
/>
<input
value={user.email}
onChange={e => updateField('email', e.target.value)}
/>
<button onClick={reset}>Reset All</button>
</div>
);
}
Which Approach to Use?
- Separate variables: Unrelated data, simple components
- Single object: Related data (form fields), need bulk updates
- Rule of thumb: If data changes together, group it
⚙️ State Batching
React 18+ automatically batches state updates for performance:
function BatchingExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Render'); // Check how many times this runs
const handleClick = () => {
setCount(c => c + 1); // Update 1
setFlag(f => !f); // Update 2
setCount(c => c + 1); // Update 3
// In React 18: All 3 updates batched → renders ONCE
// In React 17: Would render multiple times
};
const handleAsyncClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18: Batched even in setTimeout!
// React 17: Would render twice
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Sync Update</button>
<button onClick={handleAsyncClick}>Async Update</button>
</div>
);
}
🎯 Array State Patterns
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Build App', done: false }
]);
// Add item
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
done: false
};
setTodos(prevTodos => [...prevTodos, newTodo]);
};
// Remove item
const removeTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
// Update item
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
// Update specific field
const updateTodoText = (id, newText) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
)
);
};
// Sort items
const sortTodos = () => {
setTodos(prevTodos => [...prevTodos].sort((a, b) =>
a.text.localeCompare(b.text)
));
};
// Clear completed
const clearCompleted = () => {
setTodos(prevTodos => prevTodos.filter(todo => !todo.done));
};
return (
<div>
{todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</div>
))}
<button onClick={sortTodos}>Sort</button>
<button onClick={clearCompleted}>Clear Completed</button>
</div>
);
}
🔄 Derived State
Don't store values you can calculate - compute them during render:
// ❌ Bad - duplicating state
function ShoppingCart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0); // Duplicate!
const addItem = (item) => {
const newItems = [...items, item];
setItems(newItems);
// Must remember to update total
setTotal(newItems.reduce((sum, i) => sum + i.price, 0));
};
// Problem: Easy to forget, state can get out of sync
}
// ✅ Good - derive during render
function ShoppingCart() {
const [items, setItems] = useState([]);
// Calculate on every render (cheap operation)
const total = items.reduce((sum, item) => sum + item.price, 0);
const addItem = (item) => {
setItems(prevItems => [...prevItems, item]);
// Total automatically updates!
};
return (
<div>
<p>Total: ${total}</p>
{/* items list */}
</div>
);
}
More Derived State Examples:
function UserList() {
const [users, setUsers] = useState([...]);
const [searchTerm, setSearchTerm] = useState('');
// Derived - no need to store separately
const filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const activeUserCount = users.filter(u => u.isActive).length;
const averageAge = users.reduce((sum, u) => sum + u.age, 0) / users.length;
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search users"
/>
<p>Active users: {activeUserCount}</p>
<p>Average age: {averageAge.toFixed(1)}</p>
<ul>
{filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
</div>
);
}
🎯 Complete Example: Form with Validation
function RegistrationForm() {
// Form state
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
// UI state
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Validation (derived)
const errors = {};
if (formData.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
}
if (!formData.email.includes('@')) {
errors.email = 'Invalid email';
}
if (formData.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
}
// Derived state
const isValid = Object.keys(errors).length === 0;
const showError = (field) => touched[field] && errors[field];
// Update field
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
// Mark field as touched
const handleBlur = (field) => {
setTouched(prev => ({ ...prev, [field]: true }));
};
// Submit
const handleSubmit = async (e) => {
e.preventDefault();
// Mark all fields as touched
setTouched({
username: true,
email: true,
password: true,
confirmPassword: true
});
if (!isValid) return;
setIsSubmitting(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
alert('Registration successful!');
// Reset form
setFormData({
username: '',
email: '',
password: '',
confirmPassword: ''
});
setTouched({});
} catch (error) {
alert('Registration failed');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
value={formData.username}
onChange={e => handleChange('username', e.target.value)}
onBlur={() => handleBlur('username')}
placeholder="Username"
/>
{showError('username') && (
<p className="error">{errors.username}</p>
)}
</div>
<div>
<input
type="email"
value={formData.email}
onChange={e => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
placeholder="Email"
/>
{showError('email') && (
<p className="error">{errors.email}</p>
)}
</div>
<div>
<input
type="password"
value={formData.password}
onChange={e => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
placeholder="Password"
/>
{showError('password') && (
<p className="error">{errors.password}</p>
)}
</div>
<div>
<input
type="password"
value={formData.confirmPassword}
onChange={e => handleChange('confirmPassword', e.target.value)}
onBlur={() => handleBlur('confirmPassword')}
placeholder="Confirm Password"
/>
{showError('confirmPassword') && (
<p className="error">{errors.confirmPassword}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</form>
);
}
⚠️ Common Mistakes
1. Mutating State Directly
// ❌ Wrong
const [user, setUser] = useState({ name: 'John', age: 30 });
user.age = 31; // Mutates state directly!
setUser(user); // React won't detect change
// ✅ Correct
setUser({ ...user, age: 31 });
2. Not Using Functional Updates
// ❌ Can miss updates
setCount(count + 1);
// ✅ Always gets latest
setCount(prev => prev + 1);
3. Too Many State Variables
// ❌ Too fragmented
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [street, setStreet] = useState('');
const [city, setCity] = useState('');
const [zip, setZip] = useState('');
// ✅ Better - group related data
const [user, setUser] = useState({
name: { first: '', last: '' },
address: { street: '', city: '', zip: '' }
});
🎯 Key Takeaways
- Lazy init: Use
() => valuefor expensive calculations - Functional updates: Use
prev => prev + 1for reliable updates - Never mutate: Always create new objects/arrays
- Derived state: Calculate during render, don't store
- State batching: React 18 batches all updates automatically
- Group related data: Use objects for form fields
- Separate unrelated data: Don't put everything in one object
- Array operations: Use map, filter, spread - never push/splice