useState & useEffect

State management and side effects — the two most essential React Hooks

useState useEffect Dependencies Cleanup

Table of Contents

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:

HookPurposeAnalogy
useStateStore and update data that changes over timeA whiteboard you can write on and erase
useEffectPerform actions that "reach outside" the componentA butler who handles tasks outside your room
JSXfunction 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

JSXimport { useState } from 'react'; function MyComponent() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
PartPurpose
countThe current state value (read-only)
setCountFunction to update the state
useState(0)Initial value

2.3 Declaring Multiple State Variables

JSXfunction 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:

JSXsetName("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:

JavaScriptfetch('/api/users'); localStorage.setItem('key', 'value'); document.title = "New Title"; console.log("Hello"); setInterval(() => {}, 1000);

3.2 Examples of Side Effects in React

TypeExample
Data Fetchingfetch(), axios.get()
DOM Manipulationdocument.title, element.focus()
TimerssetTimeout(), setInterval()
SubscriptionsWebSockets, event listeners
Browser StoragelocalStorage.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

JSXuseEffect(() => { // Side effect code — runs AFTER render return () => { // Cleanup (optional) — runs before unmount or next effect }; }, [dependencies]);
PartWhen it runs
Effect functionAfter render (when deps change or every render if no deps)
CleanupBefore unmount OR before next effect runs
Dependency arrayControls when the effect re-runs

4.3 When Does useEffect Run?

JSXuseEffect(() => { 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)

JSXuseEffect(() => { 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)

JSXuseEffect(() => { 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)

JSXuseEffect(() => { 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.

JSXuseEffect(() => { const interval = setInterval(() => setSeconds(prev => prev + 1), 1000); return () => clearInterval(interval); }, []);

6.2 Common Cleanup Scenarios

Timer cleanup:

JSXuseEffect(() => { if (seconds <= 0) return; const timer = setTimeout(() => setSeconds(prev => prev - 1), 1000); return () => clearTimeout(timer); }, [seconds]);

Event listener cleanup:

JSXuseEffect(() => { const handleResize = () => setWindowSize({ width: window.innerWidth, height: window.innerHeight }); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []);

Fetch abort cleanup:

JSXuseEffect(() => { 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

JSXfunction 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

JSXfunction 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>; }

7.3 Example 3: Form with Validation

JSXfunction 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

JSXfunction 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

JSXfunction 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

MistakeProblemSolution
Missing dependenciesStale dataInclude all used state/props in deps
Empty deps for changing logicEffect doesn't updateAdd correct dependencies
No cleanupMemory leaksReturn cleanup function
Mutating state directlyNo re-renderAlways use setter
Conditional hooksBreaks React trackingCall hooks at top level
Infinite loop in useEffectRe-renders foreverFix deps and state updates
Stale closureOld values in effectInclude 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:

APrimitive, object, or array
BOnly strings
COnly functions
DOnly null
Explanation: Any serializable state shape.
2

Lazy initial state: useState(() => expensive()) means:

AInitializer runs once on mount
BRuns every render
CRuns never
DReplaces useEffect
Explanation: Function form avoids recomputing initial state.
3

useEffect with empty deps [] runs:

AOnce after mount (plus strict dev re-run)
BEvery keystroke
CBefore mount
DNever
Explanation: Mount-only side effects use [].
4

Omitting useEffect dependency array:

ARuns after every render
BRuns never
CRuns once only
DRuns before render
Explanation: No array means effect on each render—usually avoid.
5

Cleanup in useEffect returns:

AFunction called on unmount or before re-run
BJSX
CPromise required
DCSS
Explanation: Return () => unsubscribe() for cleanup.
6

Fetching data in useEffect should handle:

ALoading, error, and abort/cancel
BOnly success path
CSynchronous only
DNo state
Explanation: Race conditions need cleanup or ignore flag.
7

setCount(c => c + 1) is:

AFunctional update
BInvalid syntax
CCSS
DProp change
Explanation: Uses previous state safely.
8

useEffect depending on [userId] re-runs when:

AuserId changes
BAny state anywhere changes always
CNever
DOnly on unmount
Explanation: Deps list controls re-execution.
9

Setting state in effect without deps carefully can:

ACause infinite loop if effect sets state each run
BAlways improve speed
CDisable React
DRemove DOM
Explanation: Include correct deps or guard updates.
10

Multiple useState calls are:

AAllowed and encouraged for separate concerns
BForbidden
COne per app only
DClasses only
Explanation: Split unrelated state variables.

10 Advanced useState & useEffect MCQs

1

AbortController in fetch effect prevents:

ASetting state after unmount or stale response
BAll network calls
CJSX compile
DKeys
Explanation: abort() in cleanup on dependency change.
2

useEffect async pattern:

ADefine async function inside effect and call it
BMake useEffect callback async directly
CUse await on useState
DNo fetch allowed
Explanation: Effect callback cannot be async function itself.
3

Object in dependency array compared by:

AReference identity (Object.is)
BDeep JSON always
CAlphabetical keys
DDOM id
Explanation: New object literal each render retriggers effect.
4

useSyncExternalStore is for:

ASubscribing to external stores safely
BCSS only
CReplacing useState always
DForms only
Explanation: Concurrent-safe external data.
5

Derived data from state should often:

ACompute during render, not duplicate in state
BAlways be second useState
CLive in ref only
DUse localStorage
Explanation: Avoid redundant synchronized state.
6

useEffect event pattern (React docs) separates:

AEffect logic from event-driven updates
BCSS from HTML
CRouter from Redux
Dnpm from node
Explanation: Not every interaction belongs in an effect.
7

interval in useEffect needs cleanup to:

AclearInterval on unmount
Bspeed up interval
Cduplicate timers
Dblock render
Explanation: Prevent memory leaks and duplicate timers.
8

State batching means two setStates in click:

AMay produce one re-render
BAlways two full page loads
CThrows
DUpdates props
Explanation: React 18 batches in more cases.
9

useDeferredValue helps:

ADefer updating expensive UI while keeping input responsive
BReplace useEffect
CDelete state
DSSR only
Explanation: Mark non-urgent updates lower priority.
10

useTransition marks updates as:

ANon-urgent transitions
BSynchronous blocking
CServer-only
DCSS animations
Explanation: startTransition(() => setTab(...)) for smoother UX.

15 useState & useEffect Interview Questions & Answers

Easy Medium Hard
1What does useState return?easy
Answer: Current state value and a setter function to update it.
2When does useEffect run?medium
Answer: After render is committed to screen; timing depends on dependency array.
3What is the cleanup function?medium
Answer: Function returned from effect, run before next effect and on unmount.
4Why empty dependency array?easy
Answer: Run effect once on mount (e.g. subscribe, initial fetch).
5How to avoid infinite useEffect loops?hard
Answer: Only set state in effect when necessary; correct deps; avoid unstable objects in deps.
6Fetch data on mount pattern?medium
Answer: useEffect with [id], set loading, fetch, set data/error, abort on cleanup.
7Functional updates benefit?medium
Answer: When new state depends on previous, especially in async or batched updates.
8useEffect vs useLayoutEffect timing?hard
Answer: useEffect after paint; useLayoutEffect synchronously after DOM updates.
9Can you use multiple useEffects?easy
Answer: Yes—split unrelated side effects for clarity.
10Stale closure in effect?hard
Answer: Missing dependency causes effect to see old values—fix deps or use functional updates.
11Lazy initial state example?medium
Answer: useState(() => JSON.parse(localStorage.getItem('x'))) runs once.
12Should every value be in state?medium
Answer: No—derive values during render when possible.
13useEffect and Strict Mode?hard
Answer: Dev may mount/unmount twice to verify cleanup—effects should be idempotent.
14Separating concerns with multiple useState?medium
Answer: Prefer separate state for unrelated data instead of one big object.
15useTransition use case?hard
Answer: Tab switches or heavy filters where UI can stay responsive during update.