useEffect & Effects
Effects run after paint, dependency arrays, cleanup timing, and the stale-closure / infinite-loop / overuse pitfalls.
🧑🏫 Sabse pehle — simple mein samjho#
Effect matlab side-kaam jo render ke BAAD chalta hai — data fetch, timer, ya React ke bahar ki duniya se baat. Dependency array [dep] batata hai kab dobara chale: dep badla toh effect phir chalega. Cleanup function jaate waqt safai karta hai (timer/subscription band). Socho: ghar aaye (render) toh AC on kiya (effect), aur jaate waqt AC off (cleanup).
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000); // side-kaam
return () => clearInterval(id); // cleanup = safai
}, []); // [] = sirf ek baar, mount pe
Yaad rakho: effect render ke baad chalta hai, aur cleanup jaate waqt safai karta hai.
What an effect is 🎯#
useEffect synchronizes a component with an external system (subscriptions, timers, DOM, network, non-React widgets). It runs after React commits and the browser paints — so it never blocks the visual update. (useLayoutEffect runs synchronously before paint for measurement.)
useEffect(() => {
document.title = `Count: ${count}`; // side effect after paint
}, [count]);
The dependency array#
| Deps arg | Effect runs |
|---|---|
| (omitted) | After every render |
[] |
Once after mount (and cleanup on unmount) |
[a, b] |
After mount + whenever a or b change (Object.is compare) |
React compares each dependency with the previous render's value using Object.is.
Cleanup functions#
Return a function; React runs it before the next effect re-runs and once on unmount. Use it to undo the effect: unsubscribe, clear timers, abort fetches.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup
}, []);
flowchart LR
M["Mount"] --> E1["Run effect"]
E1 --> D{"Deps changed?"}
D -->|Yes| C1["Run cleanup"] --> E2["Run effect again"] --> D
D -->|No| W["Wait"] --> D
E2 --> U["Unmount"]
W --> U
U --> C2["Final cleanup"]
Common uses#
- Subscribing to a store / WebSocket / event listener (with cleanup)
setInterval/setTimeout(clear in cleanup)- Imperatively syncing a non-React library to props
- Data fetching (though prefer a framework/query lib — see below)
Real-world example: fetching a restaurant menu (Zomato/Swiggy)#
A typical home-screen effect: fetch the restaurant/menu list once on mount, track loading and error state, and render accordingly.
function RestaurantList() {
const [restaurants, setRestaurants] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch("/api/restaurants")
.then(res => {
if (!res.ok) throw new Error("Failed to load restaurants");
return res.json();
})
.then(data => setRestaurants(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []); // once on mount, like opening the app's home screen
if (loading) return <p>Loading restaurants near you…</p>;
if (error) return <p>⚠️ {error}</p>;
return (
<ul>
{restaurants.map(r => <li key={r.id}>{r.name} — {r.cuisine}</li>)}
</ul>
);
}
Race conditions when a dependency changes fast#
If the effect re-runs on a changing dependency — e.g. a search box filtering restaurants by query — a slow earlier request can resolve after a faster later one, overwriting fresh data with stale results.
useEffect(() => {
let ignore = false; // 🟢 flag flipped by cleanup
fetch(`/api/restaurants?q=${query}`)
.then(res => res.json())
.then(data => {
if (!ignore) setRestaurants(data); // ignore stale responses
});
return () => { ignore = true; }; // runs before next effect / unmount
}, [query]);
flowchart TD
Q1["query = piz"] --> F1["Fetch #1 starts, slow"]
Q2["query = pizza, typed fast"] --> CU["Cleanup sets ignore = true for #1"]
CU --> F2["Fetch #2 starts"]
F1 -->|"resolves late"| R1{"ignore true?"}
R1 -->|"yes, skip"| SKIP["Stale response discarded"]
F2 -->|"resolves"| R2["setRestaurants with fresh data"]
🟢 An AbortController achieves the same result more actively — call controller.abort() in cleanup so the in-flight fetch itself is cancelled, not just its result ignored.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/restaurants?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setRestaurants)
.catch(err => { if (err.name !== "AbortError") setError(err.message); });
return () => controller.abort();
}, [query]);
Pitfalls#
⚠️ Stale closures from missing deps. An effect closes over the values from the render it ran in. Omit a dep and it keeps reading the old value.
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000); // ⚠️ count frozen at 0
return () => clearInterval(id);
}, []); // missing count
// 🟢 functional update removes the dependency
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
⚠️ Object/function deps cause infinite loops. A fresh object/array/function each render is a new reference, so the effect re-runs, which often sets state, which re-renders… Fix by depending on primitives, or memoizing with useMemo/useCallback, or moving the object inside the effect.
⚠️ Don't overuse effects. If a value can be computed during render from props/state, do that — no effect needed. Effects for derived state add a redundant render and bugs.
// ⚠️ effect to derive state
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// 🟢 just compute during render
const full = `${first} ${last}`;
🟢 Reach for effects only to sync with outside systems; for events, put the logic in the event handler, not an effect.
Interview Q&A#
Q1. When does an effect run relative to paint?
After React commits and the browser paints, so it doesn't block rendering. useLayoutEffect runs synchronously before paint for DOM measurement.
Q2. What do [], [dep], and no array mean?
[] runs once on mount; [dep] runs on mount and whenever dep changes; omitting the array runs after every render.
Q3. When does cleanup run? Before the effect re-executes on a dependency change, and once when the component unmounts.
Q4. What causes a stale closure? Omitting a value the effect reads from the dependency array freezes it at its render-time value. Fix by adding the dep or using a functional state update.
Q5. Why might an effect loop forever? A non-primitive dependency (object/array/function) recreated each render is a new reference, re-triggering the effect. Memoize it or depend on primitives.
Q6. Sign you're overusing effects? Using an effect to compute state from other state/props. Derive it during render instead of storing it and syncing.