1. Introduction 👋
useEffect is React's primary tool for synchronizing components with external systems — APIs, timers, subscriptions, and the DOM itself. This tutorial provides a thorough exploration of how useEffect works, when to use it, and the common pitfalls that trip up even experienced developers.
Information
2. What is useEffect? 🤔
useEffect is a Hook that lets a component run code after rendering, typically to synchronize with something outside of React's control — like fetching data, subscribing to an event, or manually manipulating the DOM.
Code Snippet
import { useEffect, useState } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(interval);
}, []);
return <p>Elapsed: {seconds}s</p>;
}3. Why Use useEffect? 💡
- External Synchronization: Keep a component in sync with APIs, browser storage, or third-party libraries.
- Lifecycle Replication: Handle mount, update, and unmount behavior in function components.
- Cleanup Management: Safely tear down subscriptions and listeners when no longer needed.
- Decoupling: Keeps side-effect logic separate from the pure rendering logic of a component.
4. Understanding Side Effects ⚡
A side effect is any operation that reaches outside of a component's rendering process — network requests, subscriptions, manual DOM manipulation, logging, and timers are all common examples.
| Not a Side Effect (Pure Render Logic) | Side Effect (Belongs in useEffect) |
|---|---|
| Computing a derived value from props/state | Fetching data from an API |
| Formatting a string for display | Subscribing to a WebSocket |
| Filtering or sorting an array | Manually setting document.title |
Important
5. Effect Lifecycle ⏳
Every effect follows the same lifecycle: it runs after render, optionally returns a cleanup function, and that cleanup runs before the effect re-runs or when the component unmounts.
6. Basic useEffect Usage 🎬
Code Snippet
import { useEffect } from 'react';
function PageTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}Tip
7. Dependency Array 📋
The dependency array is the second argument to useEffect. It tells React when to re-run the effect — only when one of the listed values has changed since the last render.
Code Snippet
useEffect(() => {
console.log("Runs when 'userId' changes");
}, [userId]);Important
8. No Dependency Array 🔁
Omitting the dependency array entirely causes the effect to run after every single render — this is rarely what you want.
Code Snippet
useEffect(() => {
console.log("Runs after every render, no matter what changed");
}); // no dependency arrayWarning
9. Empty Dependency Array 🌱
Passing an empty array [] tells React the effect depends on nothing, so it runs only once, after the initial mount.
Code Snippet
useEffect(() => {
console.log("Runs only once, on mount");
}, []);Note
10. Multiple Dependencies 🔢
Code Snippet
useEffect(() => {
console.log(`Fetching results for "${query}" on page ${page}`);
fetchResults(query, page);
}, [query, page]); // re-runs when EITHER query or page changesTip
11. Cleanup Function 🧹
An effect can return a function, which React calls before the effect re-runs and again when the component unmounts — this is where subscriptions, timers, and listeners get torn down.
Code Snippet
useEffect(() => {
const interval = setInterval(() => console.log("tick"), 1000);
return () => {
clearInterval(interval); // cleanup
};
}, []);Important
12. Mounting Effects 🌱
Code Snippet
function AnalyticsTracker({ pageName }) {
useEffect(() => {
trackPageView(pageName);
}, []); // runs once, when the component first mounts
return null;
}Caution
13. Updating Effects 🔄
Code Snippet
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchSearchResults(query).then(setResults);
}, [query]); // re-runs every time 'query' changes
return <ResultsList results={results} />;
}14. Unmounting Effects 🍂
Code Snippet
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = connectToRoom(roomId);
return () => {
connection.disconnect(); // runs when the component unmounts
};
}, [roomId]);
}Note
15. Fetching Data 🌐
Code Snippet
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (!ignore) setUser(data);
});
return () => {
ignore = true; // prevents setting state from a stale request
};
}, [userId]);
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}Best Practice
16. Event Listeners 🖱️
Code Snippet
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <p>Window width: {width}px</p>;
}17. Timers and Intervals ⏱️
Code Snippet
function Countdown({ seconds }) {
const [remaining, setRemaining] = useState(seconds);
useEffect(() => {
if (remaining <= 0) return;
const timeout = setTimeout(() => setRemaining((r) => r - 1), 1000);
return () => clearTimeout(timeout);
}, [remaining]);
return <p>{remaining}s remaining</p>;
}Tip
18. Subscriptions 📡
Code Snippet
function OnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return <p>{isOnline ? "🟢 Online" : "🔴 Offline"}</p>;
}19. Synchronizing with External Systems 🔗
The React team frames useEffect's core purpose as synchronizing a component with a system outside of React — not as a general-purpose "run this code" hook.
- Third-party UI libraries (maps, charts, editors) that manage their own DOM.
- Browser APIs like localStorage, geolocation, or media devices.
- Network connections such as WebSockets or Server-Sent Events.
Reference
20. Preventing Infinite Loops ♾️
An infinite loop occurs when an effect updates a piece of state that's also listed in its own dependency array (or omitted incorrectly), causing it to re-run indefinitely.
❌ Infinite Loop
function Buggy() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // triggers a re-render, which re-runs the effect again
}, [count]); // 🔥 infinite loop
}✅ Fixed with a Functional Update and Correct Trigger
function Fixed() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(interval);
}, []); // runs once, updates safely on a timer
}Danger
21. Dependency Management 📐
- Include every value from component scope that the effect actually reads.
- Use the eslint-plugin-react-hooks exhaustive-deps rule to catch missing dependencies automatically.
- If a function is recreated every render and used in an effect, wrap it in useCallback or move it inside the effect.
Code Snippet
function Search({ query, onResults }) {
useEffect(() => {
fetchResults(query).then(onResults);
}, [query, onResults]); // 'onResults' must be memoized in the parent to avoid re-running unnecessarily
}22. Stale Closures 🕰️
A stale closure happens when an effect captures an old value from a previous render and continues using it, usually because that value was missing from the dependency array.
❌ Stale Closure
function Chat({ message }) {
useEffect(() => {
const interval = setInterval(() => {
console.log(message); // always logs the INITIAL message
}, 5000);
return () => clearInterval(interval);
}, []); // ❌ missing 'message' dependency
}✅ Fixed
function Chat({ message }) {
useEffect(() => {
const interval = setInterval(() => {
console.log(message); // always logs the LATEST message
}, 5000);
return () => clearInterval(interval);
}, [message]); // ✅ effect re-subscribes with the current message
}23. Splitting Effects ✂️
When a component has multiple unrelated side effects, split them into separate useEffect calls rather than combining everything into one large effect.
Code Snippet
function Profile({ userId, roomId }) {
useEffect(() => {
document.title = `User ${userId}`;
}, [userId]);
useEffect(() => {
const connection = connectToRoom(roomId);
return () => connection.disconnect();
}, [roomId]);
}Best Practice
24. Custom Hooks with useEffect 🛠️
Wrapping useEffect-based logic in a custom Hook makes common synchronization patterns reusable across many components.
useWindowSize.js
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return size;
}
// Usage
function App() {
const { width, height } = useWindowSize();
return <p>{width} x {height}</p>;
}25. Performance Considerations ⚡
- Avoid unnecessary effects — many operations can be handled directly during render or in event handlers instead.
- Keep dependency arrays as minimal and accurate as possible to avoid unnecessary re-runs.
- Use useMemo/useCallback to stabilize object/function dependencies passed into effects.
- Prefer useLayoutEffect only when you must measure or mutate the DOM before the browser paints — it blocks visual updates.
Caution
26. TypeScript with useEffect 🔷
Code Snippet
function useDocumentTitle(title: string): void {
useEffect(() => {
document.title = title;
}, [title]);
}
function useInterval(callback: () => void, delay: number | null): void {
useEffect(() => {
if (delay === null) return;
const id = setInterval(callback, delay);
return () => clearInterval(id);
}, [callback, delay]);
}Tip
27. Best Practices 🌟
- Use useEffect only for genuine side effects, not for computing derived values.
- Always include every value the effect reads in its dependency array.
- Return a cleanup function for any subscription, timer, or listener.
- Split unrelated concerns into separate effects.
- Extract reusable effect logic into custom Hooks.
- Enable eslint-plugin-react-hooks to catch dependency mistakes automatically.
28. Common Mistakes 🚫
- Omitting values from the dependency array, causing stale closures.
- Forgetting a cleanup function, leading to memory leaks and duplicate subscriptions.
- Using useEffect for logic that could be computed directly during render.
- Updating state unconditionally inside an effect that depends on that same state, causing an infinite loop.
- Not guarding against race conditions in data-fetching effects.
Danger
29. Frequently Asked Questions ❓
Question
Answer
Question
Answer
Question
Answer
30. Summary 📝
useEffect lets components synchronize with the outside world — fetching data, managing subscriptions, and interacting with browser APIs. Understanding its dependency array, cleanup function, and common pitfalls like stale closures and infinite loops is essential for writing robust, leak-free React applications.