React Hooks — useState
Difficulty: 🟢 Easy | Frequency: ⭐⭐⭐⭐⭐ | Companies: Google, Meta, Amazon, Microsoft, Netflix
What is useState?
useState is a Hook that lets you add state to functional components. It returns an array: [state, setState].
State persists between re-renders and triggers UI updates when changed via the setter function.
Core Patterns
import { useState } from 'react';
// 1. Basic usage
const [count, setCount] = useState(0);
// 2. Lazy initialization (expensive computations run once)
const [data, setData] = useState(() => computeExpensiveValue());
// 3. Functional updates — use when new state depends on old state
setCount(prev => prev + 1);
// 4. Object state — always spread to preserve other fields
const [user, setUser] = useState({ name: '', age: 0 });
setUser(prev => ({ ...prev, name: 'Alice' }));
// 5. Array state
const [todos, setTodos] = useState([]);
setTodos(prev => [...prev, { id: Date.now(), text: 'New item' }]);
setTodos(prev => prev.filter(t => t.id !== id)); // remove
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: true } : t)); // update
When to Use Functional Updates
Use setCount(prev => prev + 1) (not setCount(count + 1)) when:
- Multiple rapid updates — each gets the latest state:
// ❌ Both reads same stale count
setCount(count + 1);
setCount(count + 1); // → count + 1 (not +2)
// ✅ Each gets fresh state
setCount(prev => prev + 1);
setCount(prev => prev + 1); // → count + 2
- Stale closures (inside
useEffect,setTimeout,setInterval):
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // ✅ Always correct
// setCount(count + 1); // ❌ Stale: count is always 0
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps — count captured at mount
Form Handling
function LoginForm() {
const [formData, setFormData] = useState({
username: '',
password: '',
rememberMe: false
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
return (
<form>
<input name="username" value={formData.username} onChange={handleChange} />
<input type="password" name="password" value={formData.password} onChange={handleChange} />
<input type="checkbox" name="rememberMe" checked={formData.rememberMe} onChange={handleChange} />
</form>
);
}
Common Mistakes
// ❌ Mutating state directly
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // React won't detect change
setItems(items); // Same reference
// ✅ Create new reference
setItems(prev => [...prev, 4]);
// ❌ Object state without spread (loses fields)
setUser({ name: 'Alice' }); // age is gone!
// ✅ Always spread
setUser(prev => ({ ...prev, name: 'Alice' }));
Internal Behavior
setStateis asynchronous — state is read after re-render, not immediately:
const handleClick = () => {
setCount(5);
console.log(count); // Still old value! Re-render hasn't happened yet
};
-
Bail-out optimization: If you call
setStatewith the same value (compared viaObject.is), React skips the re-render. This works for primitives but not for objects/arrays (new reference = re-render). -
React 18 automatic batching: Multiple
setStatecalls are batched into a single re-render everywhere (not just event handlers).
useState vs useReducer
Use useState | Use useReducer |
|---|---|
| 1–3 independent values | 4+ related values |
| Simple updates | Complex transition logic |
| No validation needed | State must validate transitions |
Real-World Bug: Race Condition in Search
// ❌ Race condition: older fetch can overwrite newer results
const handleSearch = async (query) => {
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data); // May arrive out of order
};
// ✅ Fix: AbortController cancels the previous request
const abortRef = useRef(null);
const handleSearch = async (query) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
const data = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
}).then(r => r.json());
setResults(data);
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
};
Follow-up Questions
- Why do we need functional updates?
- What's the difference between useState and useReducer?
- How does React know when to re-render?
- What happens if you call setState with the same value?
- Can you batch multiple setState calls?
Content from Frontend-Master-Prep-Series — 03-react