useEffect - Side Effects in React
Documentation for useEffect - Side Effects in React.
useEffect - Side Effects in React
What is useEffect?
useEffect lets you perform side effects in functional components.
Definition: Side effects are operations that affect something outside the component's render: data fetching, subscriptions, manually changing the DOM, logging, timers, etc. The useEffect hook runs after React renders the component, allowing you to synchronize with external systems while keeping the render function pure.
Why Use useEffect?
| Purpose | Example |
|---|---|
| Data Fetching | API calls, loading data |
| Subscriptions | WebSocket, event listeners |
| DOM Manipulation | Focus input, update document title |
| Timers | setTimeout, setInterval |
| Logging/Analytics | Track page views, user actions |
| Local Storage | Save/load persisted data |
useEffect Lifecycle
┌─────────────────────────────────────────────────────────┐
│ useEffect Timing │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. Component Renders │
│ │ │
│ ↓ │
│ 2. React Updates DOM │
│ │ │
│ ↓ │
│ 3. Browser Paints Screen │
│ │ │
│ ↓ │
│ 4. useEffect Runs (after paint) │
│ │ │
│ ↓ │
│ [On next render with changed deps] │
│ │ │
│ 5. Cleanup Function Runs (if provided) │
│ │ │
│ ↓ │
│ 6. New Effect Runs │
│ │
└─────────────────────────────────────────────────────────┘Basic Syntax
import { useEffect, useState } from "react";
function Component() {
const [count, setCount] = useState(0);
// 1. No dependency array - runs after EVERY render
useEffect(() => {
console.log("Runs after every render");
});
// 2. Empty dependency array - runs ONCE on mount
useEffect(() => {
console.log("Runs only on mount");
}, []);
// 3. With dependencies - runs when dependencies change
useEffect(() => {
console.log("Runs when count changes:", count);
}, [count]);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Dependency Array Explained
| Dependency Array | When Effect Runs | Use Case |
|---|---|---|
| Not provided | After every render | Rarely needed |
[] | Once on mount | Initial data fetch |
[a, b] | On mount + when a or b changes | React to state/prop |
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Runs when userId prop changes
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Re-fetch when userId changes
return <div>{user?.name}</div>;
}Cleanup Function
Return a function to clean up side effects (prevent memory leaks).
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Setup
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// Cleanup - runs before effect re-runs or on unmount
return () => {
clearInterval(interval);
};
}, []); // Empty deps = setup once, cleanup on unmount
return <p>Seconds: {seconds}</p>;
}When Cleanup Runs
useEffect(() => {
console.log("Effect runs");
return () => {
console.log("Cleanup runs");
};
}, [dependency]);
// Flow:
// 1. Mount: "Effect runs"
// 2. dependency changes: "Cleanup runs" → "Effect runs"
// 3. dependency changes again: "Cleanup runs" → "Effect runs"
// 4. Unmount: "Cleanup runs"Common Use Cases
1. Data Fetching
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // Prevent state update on unmounted component
async function fetchUsers() {
try {
setLoading(true);
const response = await fetch("/api/users");
const data = await response.json();
if (isMounted) {
setUsers(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchUsers();
return () => {
isMounted = false; // Cleanup
};
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}2. Event Listeners
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", handleResize);
// Cleanup: remove listener on unmount
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return (
<p>
{size.width} x {size.height}
</p>
);
}3. Document Title
function PageTitle({ title }) {
useEffect(() => {
const prevTitle = document.title;
document.title = title;
// Optional: restore previous title on unmount
return () => {
document.title = prevTitle;
};
}, [title]);
return null; // No UI, just side effect
}4. Local Storage Sync
function ThemeToggle() {
const [theme, setTheme] = useState(() => {
return localStorage.getItem("theme") || "light";
});
// Sync to localStorage when theme changes
useEffect(() => {
localStorage.setItem("theme", theme);
document.body.className = theme;
}, [theme]);
return (
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
Toggle Theme: {theme}
</button>
);
}5. Subscriptions (WebSocket)
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection(roomId);
connection.on("message", (message) => {
setMessages((prev) => [...prev, message]);
});
connection.connect();
// Cleanup: disconnect when roomId changes or unmount
return () => {
connection.disconnect();
};
}, [roomId]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}Multiple Effects
Separate concerns into different useEffect calls.
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// Effect 1: Fetch user data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Effect 2: Fetch user's posts
useEffect(() => {
fetchPosts(userId).then(setPosts);
}, [userId]);
// Effect 3: Update document title
useEffect(() => {
if (user) {
document.title = `${user.name}'s Dashboard`;
}
}, [user]);
// Effect 4: Analytics
useEffect(() => {
trackPageView("dashboard", userId);
}, [userId]);
return <div>{/* UI */}</div>;
}useLayoutEffect
Runs synchronously after DOM mutations, before browser paint.
import { useLayoutEffect, useRef, useState } from "react";
function Tooltip({ children, text }) {
const ref = useRef();
const [position, setPosition] = useState({ top: 0, left: 0 });
// useLayoutEffect for DOM measurements (avoids flicker)
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}, []);
return (
<>
<span ref={ref}>{children}</span>
<div style={{ position: "absolute", ...position }}>{text}</div>
</>
);
}| Hook | Runs | Use Case |
|---|---|---|
useEffect | After paint (async) | Most side effects |
useLayoutEffect | Before paint (sync) | DOM measurements, animations |
Common Mistakes & Exceptions
1. Missing Dependencies
// ❌ Missing dependency causes stale closure
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // Always logs initial value!
}, 1000);
return () => clearInterval(interval);
}, []); // Missing `count` in deps
// ✅ Include all dependencies
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1); // Functional update doesn't need count in deps
}, 1000);
return () => clearInterval(interval);
}, []);2. Object/Array Dependencies
// ❌ New object reference every render = infinite loop
useEffect(() => {
fetchData(options);
}, [{ page: 1 }]); // New object every render!
// ✅ Use primitive values or memoize
const page = 1;
useEffect(() => {
fetchData({ page });
}, [page]);
// Or use useMemo
const options = useMemo(() => ({ page }), [page]);
useEffect(() => {
fetchData(options);
}, [options]);3. Async Function Directly in useEffect
// ❌ Can't use async directly
useEffect(async () => {
const data = await fetchData(); // Returns promise, not cleanup
}, []);
// ✅ Define async function inside
useEffect(() => {
async function fetchData() {
const data = await fetch("/api/data");
setData(data);
}
fetchData();
}, []);
// ✅ Or use IIFE
useEffect(() => {
(async () => {
const data = await fetch("/api/data");
setData(data);
})();
}, []);4. Setting State in Effect Without Conditions
// ❌ Infinite loop: render → effect → setState → render → ...
function Component({ value }) {
const [processed, setProcessed] = useState(value);
useEffect(() => {
setProcessed(value * 2); // Runs every render!
}); // No deps = runs after every render
}
// ✅ Add dependencies
useEffect(() => {
setProcessed(value * 2);
}, [value]); // Only when value changes
// ✅ Or calculate during render (no effect needed)
const processed = value * 2;5. Race Conditions in Data Fetching
// ❌ Race condition: fast click can show stale data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// ✅ Cancel outdated requests
useEffect(() => {
let cancelled = false;
fetchUser(userId).then((user) => {
if (!cancelled) {
setUser(user);
}
});
return () => {
cancelled = true;
};
}, [userId]);
// ✅ Or use AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((err) => {
if (err.name !== "AbortError") {
setError(err);
}
});
return () => controller.abort();
}, [userId]);Interview Questions & Answers
Q1: What is useEffect and when should you use it?
useEffect is a hook for performing side effects in functional components. Side effects are operations that interact with the outside world: API calls, event listeners, timers, DOM manipulation, logging. Use it when you need to synchronize your component with external systems. It runs after React commits changes to the DOM, keeping the render function pure. Without useEffect, you'd need class components with lifecycle methods.
Q2: How does the dependency array work?
The dependency array tells React when to re-run the effect. No array: runs after every render (rarely needed). Empty array []: runs once on mount, cleanup on unmount (like componentDidMount + componentWillUnmount). With values [a, b]: runs on mount and whenever any dependency changes. React compares dependencies using Object.is, so objects/arrays need memoization to prevent unnecessary runs. Always include all values from component scope used in the effect.
Q3: What is the cleanup function and why is it important?
The cleanup function is returned from useEffect and runs before the effect re-runs and when the component unmounts. It prevents memory leaks and stale behavior. Essential for: clearing timers/intervals, removing event listeners, cancelling subscriptions, aborting fetch requests, invalidating stale closures. Without cleanup, listeners accumulate, timers continue after unmount, and resources aren't released. Always clean up what you set up.
Q4: What's the difference between useEffect and useLayoutEffect?
Both run after DOM updates, but: useEffect runs asynchronously after the browser paints - the user sees the initial render before the effect runs. useLayoutEffect runs synchronously before paint - the browser waits for it. Use useLayoutEffect when you need to measure/mutate the DOM before the user sees it (prevents flicker). Use useEffect for everything else - it doesn't block the browser. useLayoutEffect can hurt performance if overused.
Q5: How do you fetch data with useEffect?
Define an async function inside useEffect (not the effect itself), call it, and handle cleanup for race conditions. Use a flag (isMounted or cancelled) to prevent setting state on unmounted components. For robust fetching, use AbortController to cancel pending requests. Handle loading and error states. For complex data fetching, consider libraries like React Query or SWR that handle caching, retries, and race conditions automatically.
Q6: Why can't you pass an async function directly to useEffect?
useEffect expects its callback to return either nothing or a cleanup function. An async function returns a Promise, not a cleanup function. This causes issues: React can't execute the cleanup, and the effect doesn't work as expected. Solution: define the async function inside the effect and call it, or use an IIFE. This makes the outer function synchronous while allowing async work inside.
Q7: What are common mistakes with useEffect dependencies?
Common mistakes: Missing dependencies causes stale closures - values never update. Object/array dependencies cause infinite loops since new references are created each render. Functions as dependencies need useCallback to prevent re-runs. Over-specifying complex objects when only one property is needed. ESLint's exhaustive-deps rule helps catch these. Use primitive values when possible, memoize objects/functions, or restructure your effect.
Q8: How do you prevent infinite loops in useEffect?
Infinite loops happen when: effect updates state that's in its own dependency array, or dependencies are new references each render. Solutions: use functional state updates (setCount(c => c + 1) doesn't need count in deps), memoize objects/arrays with useMemo, memoize functions with useCallback, extract primitive values from objects, add conditions before setState, or verify your logic doesn't create update cycles.
Q9: How do you handle race conditions in data fetching?
Race conditions occur when multiple requests overlap and a slower early request resolves after a faster later one, showing stale data. Solutions: Boolean flag - set cancelled = true in cleanup, check before setState. AbortController - abort pending requests in cleanup. Request ID - only update state if the current request ID matches. Libraries like React Query handle this automatically with query invalidation and cancellation.
Q10: When should you NOT use useEffect?
Avoid useEffect for: Derived state - calculate during render instead. Event handlers - put logic in the handler, not an effect syncing state. Transforming data - compute in render or useMemo. Subscribing to props - usually a misuse, pass handlers instead. Effects are for synchronization with external systems, not for running code on render. If you're using effect to update state based on other state/props, you probably don't need an effect.