How to Use useEffect in React
What you’ll build or solve
You’ll run side effects in a React component at the right time, like syncing with browser APIs, timers, or event listeners.
When this approach works best
useEffect works best when you need to:
Learn React on Mimo
- Sync with something outside React, like
document.title,localStorage, or a third-party widget. - Set up and clean up timers, event listeners, or subscriptions.
- React to a specific prop or state change with a side effect, like updating the URL or focusing an input.
Skip useEffect when you can compute the value during render. If the result depends only on props and state, derive it directly instead of storing it via an effect.
Prerequisites
- A React app set up (Vite, CRA, Next.js, etc.)
- Basic React knowledge (components, props, state)
Step-by-step instructions
1) Add an effect that runs after render
useEffect runs after React finishes rendering and updates the DOM. Put your side-effect code in the callback.
JavaScript
import {useEffect,useState }from"react";
exportdefaultfunctionProfile() {
const [name,setName]=useState("Sam");
useEffect(() => {
document.title=`Profile:${name}`;
});
return (
<div>
<p>{name}</p>
<buttononClick={() =>setName("Alex")}>Change name</button>
</div>
);
}
Without a dependency array, the effect runs after every render. That works for cheap sync work, but it can be too much for effects that set up ongoing work.
2) Control when effects run with the dependency array
The dependency array (the second argument) changes when the effect runs.
Run once on mount
Use [] to run the effect when the component mounts.
JavaScript
import {useEffect }from"react";
exportdefaultfunctionWelcome() {
useEffect(() => {
console.log("Mounted");
}, []);
return<p>Welcome</p>;
}
Run when values change
List the values your effect uses. React reruns the effect when any dependency changes.
JavaScript
import {useEffect,useState }from"react";
exportdefaultfunctionCounterTitle() {
const [count,setCount]=useState(0);
useEffect(() => {
document.title=`Count:${count}`;
}, [count]);
return (
<buttononClick={() =>setCount((c) =>c+1)}>
Count: {count}
</button>
);
}
Run after every render (rare)
Omit the array when you truly want the effect after every render.
JavaScript
useEffect(() => {
console.log("Rendered");
});
Use this sparingly. If you see repeated work you did not expect, a missing dependency array is often the reason.
What to look for:
- Include every prop and state value you read inside the effect in the dependency array.
- If a dependency changes every render (like an inline object or function), the effect will also rerun every render.
3) Clean up side effects with a return function
If your effect sets up something that keeps running, return a cleanup function. React runs cleanup before rerunning the effect and when the component unmounts.
JavaScript
import {useEffect,useState }from"react";
exportdefaultfunctionWindowWidth() {
const [width,setWidth]=useState(() =>window.innerWidth);
useEffect(() => {
functiononResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize",onResize);
return () => {
window.removeEventListener("resize",onResize);
};
}, []);
return<p>Width: {width}px</p>;
}
What to look for:
- Cleanup runs before the next effect run, not just on unmount.
- Timers, listeners, subscriptions, and in-flight async work should not keep running after unmount.
Examples you can copy
Example 1: Sync document.title with state
JavaScript
import {useEffect,useState }from"react";
exportdefaultfunctionTitleSync() {
const [name,setName]=useState("Sam");
useEffect(() => {
document.title=`Hello,${name}`;
}, [name]);
return (
<label>
Name:{" "}
<input
value={name}
onChange={(e) =>setName(e.target.value)}
/>
</label>
);
}
Example 2: Start and stop a timer
import {useEffect,useState }from"react";
exportdefaultfunctionStopwatch() {
const [seconds,setSeconds]=useState(0);
const [running,setRunning]=useState(false);
useEffect(() => {
if (!running)return;
constid=setInterval(() => {
setSeconds((s) =>s+1);
},1000);
return () =>clearInterval(id);
}, [running]);
return (
<div>
<p>{seconds}s</p>
<buttononClick={() =>setRunning(true)}>Start</button>
<buttononClick={() =>setRunning(false)}>Stop</button>
<buttononClick={() =>setSeconds(0)}>Reset</button>
</div>
);
}
Example 3: Subscribe to a browser event
JavaScript
import {useEffect,useState }from"react";
exportdefaultfunctionOnlineStatus() {
const [online,setOnline]=useState(() =>navigator.onLine);
useEffect(() => {
functiononOnline() {
setOnline(true);
}
functiononOffline() {
setOnline(false);
}
window.addEventListener("online",onOnline);
window.addEventListener("offline",onOffline);
return () => {
window.removeEventListener("online",onOnline);
window.removeEventListener("offline",onOffline);
};
}, []);
return<p>{online?"Online":"Offline"}</p>;
}
Common mistakes and how to fix them
Mistake 1: Creating an infinite loop by updating state every render
What you might do:
useEffect(() => {
setCount(count+1);
});
Why it breaks:
The effect runs after render, updates state, which triggers another render, and repeats.
Fix: Add a dependency array and use a functional update when you only need “increment once” behavior.
useEffect(() => {
setCount((c) =>c+1);
}, []);
Mistake 2: Missing dependencies (stale values inside the effect)
What you might do:
useEffect(() => {
console.log("Query:",query);
}, []);
Why it breaks:
The effect captures the initial query, so later updates never reach it.
Fix: Include what you use.
useEffect(() => {
console.log("Query:",query);
}, [query]);
Troubleshooting
- If you see an effect firing twice in development, check React Strict Mode. React 18 can run effects twice in dev to surface unsafe side effects. Test a production build to confirm real behavior.
- If you see “Maximum update depth exceeded,” look for an effect that updates state without a dependency array, or dependencies that change every render.
- If you see “Can’t perform a React state update on an unmounted component,” add cleanup. Timers, listeners, subscriptions, and async work should stop on unmount.
- If an effect reruns “for no reason,” check for unstable dependencies like inline objects and functions. Move them inside the effect, or memoize them with
useMemooruseCallback.
Quick recap
- Put side effects inside
useEffect(() => { ... }). - Use
[]for mount-only effects, and[deps]to rerun when values change. - Return a cleanup function to stop timers, listeners, and subscriptions.
- Include every value you read inside the effect in the dependency array.
- Watch for infinite loops and Strict Mode double-runs in development
Join 35M+ people learning for free on Mimo
4.8 out of 5 across 1M+ reviews
Check us out on Apple AppStore, Google Play Store, and Trustpilot