The Complete useEffect Guide for Junior React Developers (with real examples)
If you have ever searched for a React useEffect tutorial, you probably were not looking for a definition. You were trying to figure out why your API request ran twice, why your event listener kept stacking up, or why a timeout fired after the component was already gone.
That confusion is normal. useEffect is usually the first React hook that forces junior developers to think about rendering, timing, and cleanup at the same time. Once you get the mental model right, the rules stop feeling random.
This guide explains what useEffect actually does, why the dependency array matters, how the useEffect cleanup function works, and how to apply it in three real examples you will see in normal React code.
What useEffect actually does: the mental model
The best mental model is simple: useEffect lets your component synchronize with something outside React. That outside thing could be a network request, a browser event, a timer, a subscription, or a third-party library.
React renders your UI first. Then, after the render is committed to the screen, React runs your effect. That means useEffect is not for calculating values you can compute during render. It is for work that has to happen because the UI now exists or because some outside system needs to stay in sync with your props or state.
This is why many junior developers overuse it. If you are transforming data for display, deriving one value from another, or handling a click event directly, you often do not need an effect at all. That overuse shows up in our post on common React mistakes junior developers make.
A good question to ask yourself is: “What outside thing am I syncing with?” If the answer is “nothing,” you probably do not need useEffect.
useEffect dependencies explained
The dependency array tells React when an effect needs to run again. React compares the current values in that array to the previous render. If one changed, React runs the effect again because your component may need to re-sync with the outside world.
Three patterns matter:
- No dependency array: the effect runs after every render.
- []: the effect runs after the first mount only.
- [userId, searchTerm]: the effect runs on mount and any time one of those values changes.
The most common bug is leaving something out because you want to “control” the effect. What you are really doing is telling React to ignore a value the effect uses. That creates stale closures, old data, and bugs that only show up after the component updates. If that sounds familiar, the debugging workflow in How to get unstuck in React will help you spot those patterns faster.
3 real useEffect examples
1. Fetch data when a prop changes
Fetching is the classic example because the component needs to stay in sync with the current userId. If the prop changes, the request should change too.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch("/api/users/" + userId, { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((error) => {
if (error.name !== "AbortError") {
console.error(error);
}
});
return () => controller.abort();
}, [userId]);
return <div>{user?.name}</div>;
}The dependency array includes userId because the request depends on it. The cleanup aborts the previous request so an outdated response is less likely to update state after the component moved on to a new user.
2. Add and remove a window event listener
Event listeners are another common case. You attach something outside React, so you also need to remove it when the component goes away.
function WindowWidth() {
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}</p>;
}This effect runs once on mount because the listener setup does not depend on changing props or state. The cleanup prevents duplicate listeners and memory leaks.
3. Start and clear a timeout
Timers are easy to forget because they look harmless. But a timeout keeps running even if the component unmounts unless you clear it.
function SavedMessage({ isSaved }) {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!isSaved) return;
setVisible(true);
const timeoutId = setTimeout(() => {
setVisible(false);
}, 2000);
return () => clearTimeout(timeoutId);
}, [isSaved]);
return visible ? <p>Changes saved</p> : null;
}Here the effect depends on isSaved. The cleanup clears the previous timeout so React does not try to update UI based on an old timer.
The useEffect cleanup function: when and why to use it
The cleanup function is the function you return from an effect. React runs it before the effect runs again and when the component unmounts. In practice, cleanup is how you undo any setup work that should not keep living forever.
Use cleanup when your effect creates something persistent: a subscription, event listener, timer, interval, WebSocket connection, or in-flight request. If your effect sets something up, cleanup usually tears it down.
That pattern matters because bugs around useEffect are rarely dramatic on day one. They usually show up as “why is this firing twice,” “why is my old data flashing,” or “why did this keep running after I left the page.”
Common mistakes junior developers make with useEffect
- Using an effect for derived values that could be calculated during render.
- Leaving dependencies out to silence the linter instead of fixing the logic.
- Forgetting cleanup for listeners, timers, and subscriptions.
- Putting unstable objects or inline functions in the dependency array without understanding why the effect re-runs.
If you keep hitting those mistakes, read our two related posts next. The React mistakes guide covers broader beginner bugs, and the getting unstuck guide shows how to debug React problems without randomly copying fixes from the internet.
The goal is not to memorize magic dependency rules. The goal is to think in terms of synchronization: what outside system exists, what values it depends on, and how to clean it up when those values change.
Want help fixing a real useEffect bug?
Book a live session with Mento and get unstuck with a senior React developer who can walk through your code, explain the bug, and help you ship the fix.
Book your React session