Skip to main content

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:

  1. 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
  1. 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

  • setState is 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 setState with the same value (compared via Object.is), React skips the re-render. This works for primitives but not for objects/arrays (new reference = re-render).

  • React 18 automatic batching: Multiple setState calls are batched into a single re-render everywhere (not just event handlers).

useState vs useReducer

Use useStateUse useReducer
1–3 independent values4+ related values
Simple updatesComplex transition logic
No validation neededState must validate transitions
// ❌ 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-Series03-react