useState & useEffect
State management and side effects — the two most essential React Hooks
useState
useEffect
Dependencies
Cleanup
1. Introduction: The Two Most Important Hooks
If you learn only two React Hooks, make them useState and useEffect. Together, they handle the two most common needs in any React application:
Hook Purpose Analogy
useStateStore and update data that changes over time A whiteboard you can write on and erase
useEffectPerform actions that "reach outside" the component A butler who handles tasks outside your room
JSX function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
2. useState Hook: Managing Component State
2.1 What is useState?
useState is a Hook that lets you add state to functional components. State is data that can change over time, and when it changes, React automatically re-renders your component.
Analogy: useState is like a vending machine button. You press it (call setState), and the machine (React) updates the display (re-renders) with the new selection.
2.2 Basic Syntax
JSX import { useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Part Purpose
countThe current state value (read-only)
setCountFunction to update the state
useState(0)Initial value
2.3 Declaring Multiple State Variables
JSX function UserForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState(0);
const [isSubscribed, setIsSubscribed] = useState(false);
const [errors, setErrors] = useState({});
return (
<form>
<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={isSubscribed} onChange={(e) => setIsSubscribed(e.target.checked)} />
</form>
);
}
2.4 Updating State (Direct vs Functional Updates)
Direct update — when new state doesn't depend on previous state:
JSX setName("John");
setIsLoggedIn(true);
setEmail("user@example.com");
Functional update — when new state depends on previous state:
JSX // PROBLEM — stale count for multiple updates
setCount(count + 1);
setCount(count + 1); // Results in +1, not +2
// SOLUTION — functional update
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Results in +2
setTodos(prev => [...prev, newTodo]);
2.5 Lazy Initialization (Expensive Initial State)
JSX // BAD — runs on every render
const [data, setData] = useState(expensiveCalculation());
// GOOD — function called only once on initial render
const [data, setData] = useState(() => expensiveCalculation());
function TodoApp() {
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
}
2.6 Updating Objects and Arrays with useState
Never mutate state directly! Always create a new object or array.
JSX // OBJECTS
const [user, setUser] = useState({ name: "John", age: 25, city: "New York" });
setUser({ ...user, age: 26 });
setUser(prev => ({ ...prev, age: prev.age + 1 }));
// ARRAYS
setTodos([...todos, newTodo]); // Add to end
setTodos([newTodo, ...todos]); // Add to beginning
setTodos(todos.filter(todo => todo.id !== id)); // Remove
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: true } : todo
));
2.7 Common useState Patterns
JSX // Pattern 1: Form state
const [formData, setFormData] = useState({ email: "", password: "" });
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Pattern 2: Toggle
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);
// Pattern 3: Loading and error states
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
3. What are Side Effects?
3.1 Pure Functions vs Side Effects
A pure function given the same input always returns the same output and does not modify anything outside its scope.
JavaScript // Pure function
function add(a, b) { return a + b; }
// Pure component
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
A side effect is anything a function does besides returning a value:
JavaScript fetch('/api/users');
localStorage.setItem('key', 'value');
document.title = "New Title";
console.log("Hello");
setInterval(() => {}, 1000);
3.2 Examples of Side Effects in React
Type Example
Data Fetching fetch(), axios.get()
DOM Manipulation document.title, element.focus()
Timers setTimeout(), setInterval()
Subscriptions WebSockets, event listeners
Browser Storage localStorage.setItem()
JSX // WRONG — side effect in component body (runs every render)
function BadComponent() {
const [count, setCount] = useState(0);
document.title = `Count: ${count}`;
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// CORRECT — side effect in useEffect
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
4. useEffect Hook: Handling Side Effects
4.1 What is useEffect?
useEffect lets you perform side effects in functional components. It tells React: "After rendering, run this code."
Analogy: useEffect is like a post-it note attached to your component. After React finishes painting the UI, it reads the note and performs the tasks listed there.
4.2 Basic Syntax
JSX useEffect(() => {
// Side effect code — runs AFTER render
return () => {
// Cleanup (optional) — runs before unmount or next effect
};
}, [dependencies]);
Part When it runs
Effect function After render (when deps change or every render if no deps)
Cleanup Before unmount OR before next effect runs
Dependency array Controls when the effect re-runs
4.3 When Does useEffect Run?
JSX useEffect(() => {
console.log("Runs after every render");
});
useEffect(() => {
console.log("Runs ONLY ONCE on mount");
}, []);
useEffect(() => {
console.log("Runs on mount AND when count changes");
}, [count]);
// Timing: Component renders → Paint to screen → useEffect runs → cleanup on next render/unmount
5. The Dependency Array: Controlling When Effects Run
5.1 No Dependency Array (Run on Every Render)
JSX useEffect(() => {
console.log("This runs on EVERY render");
}); // No dependency array
// When to use: Rarely — can cause performance issues
5.2 Empty Dependency Array (Run Once on Mount)
JSX useEffect(() => {
fetch('/api/initial-data')
.then(res => res.json())
.then(data => setData(data));
}, []); // Empty array = run once on mount
// Common uses: initial fetch, subscriptions, event listeners, timers
5.3 With Dependencies (Run When Dependencies Change)
JSX useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
}, [userId]);
useEffect(() => {
setLoading(true);
fetch(`/api/search?q=${query}&cat=${category}&page=${page}`)
.then(res => res.json())
.then(data => { setResults(data); setLoading(false); });
}, [query, category, page]);
5.4 The "Exhaustive Dependencies" Rule
React's ESLint rule react-hooks/exhaustive-deps warns when you're missing dependencies.
JSX // WRONG — missing userId
useEffect(() => {
fetchUser(userId).then(setUser);
}, []);
// CORRECT
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
Include in deps: state variables, props, context values used in the effect.
Don't include: state setters (stable), variables defined outside the component, ref.current.
6. Cleanup Function in useEffect
6.1 Why Cleanup is Important
Some side effects need cleanup to prevent memory leaks: timers, subscriptions, ongoing fetch requests, and DOM mutations.
JSX useEffect(() => {
const interval = setInterval(() => setSeconds(prev => prev + 1), 1000);
return () => clearInterval(interval);
}, []);
6.2 Common Cleanup Scenarios
Timer cleanup:
JSX useEffect(() => {
if (seconds <= 0) return;
const timer = setTimeout(() => setSeconds(prev => prev - 1), 1000);
return () => clearTimeout(timer);
}, [seconds]);
Event listener cleanup:
JSX useEffect(() => {
const handleResize = () => setWindowSize({
width: window.innerWidth, height: window.innerHeight
});
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Fetch abort cleanup:
JSX useEffect(() => {
const abortController = new AbortController();
fetch(`/api/users/${userId}`, { signal: abortController.signal })
.then(res => res.json())
.then(setUser)
.catch(err => { if (err.name !== 'AbortError') console.error(err); });
return () => abortController.abort();
}, [userId]);
WebSocket and debounce cleanup:
JSX // WebSocket
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/room/${roomId}`);
ws.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
return () => ws.close();
}, [roomId]);
// Debounce
useEffect(() => {
const timer = setTimeout(() => setDebouncedTerm(searchTerm), 500);
return () => clearTimeout(timer);
}, [searchTerm]);
7. Complete Working Examples
7.1 Example 1: Counter with Multiple useState
JSX function CounterApp() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const [history, setHistory] = useState([]);
const increment = () => {
setCount(prev => prev + step);
setHistory(prev => [...prev, `+${step}`]);
};
const decrement = () => {
setCount(prev => prev - step);
setHistory(prev => [...prev, `-${step}`]);
};
const reset = () => {
setCount(0);
setHistory([]);
};
return (
<div>
<p>Count: {count}</p>
<input type="number" value={step} onChange={e => setStep(Number(e.target.value))} />
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<ul>{history.map((h, i) => <li key={i}>{h}</li>)}</ul>
</div>
);
}
7.2 Example 2: Data Fetching with useEffect
JSX function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => { if (!res.ok) throw new Error('Failed'); return res.json(); })
.then(setUser)
.catch(err => { if (err.name !== 'AbortError') setError(err.message); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [userId]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user?.name}</div>;
}
JSX function SignupForm() {
const [form, setForm] = useState({ email: "", password: "" });
const [errors, setErrors] = useState({});
const validate = () => {
const next = {};
if (!form.email.includes('@')) next.email = "Invalid email";
if (form.password.length < 8) next.password = "Min 8 characters";
setErrors(next);
return Object.keys(next).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) console.log("Submit", form);
};
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email} onChange={handleChange} />
{errors.email && <span>{errors.email}</span>}
<input name="password" type="password" value={form.password} onChange={handleChange} />
{errors.password && <span>{errors.password}</span>}
<button type="submit">Sign Up</button>
</form>
);
}
7.4 Example 4: Timer with Cleanup
JSX function Timer() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const id = setInterval(() => setSeconds(s => s + 1), 1000);
return () => clearInterval(id);
}, [running]);
return (
<div>
<p>{seconds}s</p>
<button onClick={() => setRunning(true)}>Start</button>
<button onClick={() => setRunning(false)}>Stop</button>
<button onClick={() => { setRunning(false); setSeconds(0); }}>Reset</button>
</div>
);
}
7.5 Example 5: Window Event Listener
JSX function WindowSizeDisplay() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => setSize({
width: window.innerWidth,
height: window.innerHeight
});
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Window: {size.width} x {size.height}</p>;
}
8. Common Mistakes and Best Practices
Mistake Problem Solution
Missing dependencies Stale data Include all used state/props in deps
Empty deps for changing logic Effect doesn't update Add correct dependencies
No cleanup Memory leaks Return cleanup function
Mutating state directly No re-render Always use setter
Conditional hooks Breaks React tracking Call hooks at top level
Infinite loop in useEffect Re-renders forever Fix deps and state updates
Stale closure Old values in effect Include deps or use useRef
JSX // DO
useEffect(() => { fetchData(userId); }, [userId]);
setCount(prev => prev + 1);
useEffect(() => {
const sub = subscribe();
return () => sub.unsubscribe();
}, []);
// DON'T
useEffect(() => { console.log(count); }, []); // missing count
user.name = 'Jane'; setUser(user); // mutation
useEffect(() => { setCount(count + 1); }); // infinite loop
9. Summary Cheat Sheet
JSX // useState
const [value, setValue] = useState(initialValue);
setCount(prev => prev + 1);
const [data, setData] = useState(() => expensiveCalculation());
setUser(prev => ({ ...prev, age: prev.age + 1 }));
setTodos([...todos, newTodo]);
// useEffect
useEffect(() => { /* effect */ });
useEffect(() => { /* once */ return () => { /* cleanup */ }; }, []);
useEffect(() => { /* when deps change */ return () => { /* cleanup */ }; }, [dep1, dep2]);
// DEPENDENCY ARRAY
// No array: Run after EVERY render
// []: Run ONCE on mount
// [a, b]: Run when a or b changes
// CLEANUP
return () => clearInterval(id);
return () => window.removeEventListener('resize', handler);
return () => abortController.abort();
return () => ws.close();
// QUICK REFERENCE
// useState: Store data that changes
// useEffect: Side effects after render
// Cleanup: Prevent memory leaks
// Dependencies: Control when effect runs
Next Steps
useContext and Context API – Global state management
useReducer – Complex state logic (like Redux)
useCallback and useMemo – Performance optimization
useRef – DOM access and mutable values
Custom Hooks – Building reusable logic
useState & useEffect MCQ Practice
10 Basic MCQs
10 Advanced MCQs
10 Basic useState & useEffect MCQs
1
useState initial value can be:
A Primitive, object, or array
B Only strings
C Only functions
D Only null
Explanation: Any serializable state shape.
2
Lazy initial state: useState(() => expensive()) means:
A Initializer runs once on mount
B Runs every render
C Runs never
D Replaces useEffect
Explanation: Function form avoids recomputing initial state.
3
useEffect with empty deps [] runs:
A Once after mount (plus strict dev re-run)
B Every keystroke
C Before mount
D Never
Explanation: Mount-only side effects use [].
4
Omitting useEffect dependency array:
A Runs after every render
B Runs never
C Runs once only
D Runs before render
Explanation: No array means effect on each render—usually avoid.
5
Cleanup in useEffect returns:
A Function called on unmount or before re-run
B JSX
C Promise required
D CSS
Explanation: Return () => unsubscribe() for cleanup.
6
Fetching data in useEffect should handle:
A Loading, error, and abort/cancel
B Only success path
C Synchronous only
D No state
Explanation: Race conditions need cleanup or ignore flag.
7
setCount(c => c + 1) is:
A Functional update
B Invalid syntax
C CSS
D Prop change
Explanation: Uses previous state safely.
8
useEffect depending on [userId] re-runs when:
A userId changes
B Any state anywhere changes always
C Never
D Only on unmount
Explanation: Deps list controls re-execution.
9
Setting state in effect without deps carefully can:
A Cause infinite loop if effect sets state each run
B Always improve speed
C Disable React
D Remove DOM
Explanation: Include correct deps or guard updates.
10
Multiple useState calls are:
A Allowed and encouraged for separate concerns
B Forbidden
C One per app only
D Classes only
Explanation: Split unrelated state variables.
10 Advanced useState & useEffect MCQs
1
AbortController in fetch effect prevents:
A Setting state after unmount or stale response
B All network calls
C JSX compile
D Keys
Explanation: abort() in cleanup on dependency change.
2
useEffect async pattern:
A Define async function inside effect and call it
B Make useEffect callback async directly
C Use await on useState
D No fetch allowed
Explanation: Effect callback cannot be async function itself.
3
Object in dependency array compared by:
A Reference identity (Object.is)
B Deep JSON always
C Alphabetical keys
D DOM id
Explanation: New object literal each render retriggers effect.
4
useSyncExternalStore is for:
A Subscribing to external stores safely
B CSS only
C Replacing useState always
D Forms only
Explanation: Concurrent-safe external data.
5
Derived data from state should often:
A Compute during render, not duplicate in state
B Always be second useState
C Live in ref only
D Use localStorage
Explanation: Avoid redundant synchronized state.
6
useEffect event pattern (React docs) separates:
A Effect logic from event-driven updates
B CSS from HTML
C Router from Redux
D npm from node
Explanation: Not every interaction belongs in an effect.
7
interval in useEffect needs cleanup to:
A clearInterval on unmount
B speed up interval
C duplicate timers
D block render
Explanation: Prevent memory leaks and duplicate timers.
8
State batching means two setStates in click:
A May produce one re-render
B Always two full page loads
C Throws
D Updates props
Explanation: React 18 batches in more cases.
9
useDeferredValue helps:
A Defer updating expensive UI while keeping input responsive
B Replace useEffect
C Delete state
D SSR only
Explanation: Mark non-urgent updates lower priority.
10
useTransition marks updates as:
A Non-urgent transitions
B Synchronous blocking
C Server-only
D CSS animations
Explanation: startTransition(() => setTab(...)) for smoother UX.
Check All Answers
15 useState & useEffect Interview Questions & Answers
Easy
Medium
Hard
1 What does useState return?easy
Answer: Current state value and a setter function to update it.
2 When does useEffect run?medium
Answer: After render is committed to screen; timing depends on dependency array.
3 What is the cleanup function?medium
Answer: Function returned from effect, run before next effect and on unmount.
4 Why empty dependency array?easy
Answer: Run effect once on mount (e.g. subscribe, initial fetch).
5 How to avoid infinite useEffect loops?hard
Answer: Only set state in effect when necessary; correct deps; avoid unstable objects in deps.
6 Fetch data on mount pattern?medium
Answer: useEffect with [id], set loading, fetch, set data/error, abort on cleanup.
7 Functional updates benefit?medium
Answer: When new state depends on previous, especially in async or batched updates.
8 useEffect vs useLayoutEffect timing?hard
Answer: useEffect after paint; useLayoutEffect synchronously after DOM updates.
9 Can you use multiple useEffects?easy
Answer: Yes—split unrelated side effects for clarity.
10 Stale closure in effect?hard
Answer: Missing dependency causes effect to see old values—fix deps or use functional updates.
11 Lazy initial state example?medium
Answer: useState(() => JSON.parse(localStorage.getItem('x'))) runs once.
12 Should every value be in state?medium
Answer: No—derive values during render when possible.
13 useEffect and Strict Mode?hard
Answer: Dev may mount/unmount twice to verify cleanup—effects should be idempotent.
14 Separating concerns with multiple useState?medium
Answer: Prefer separate state for unrelated data instead of one big object.
15 useTransition use case?hard
Answer: Tab switches or heavy filters where UI can stay responsive during update.