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.
JSX
import { useEffect, useState } from "react";
export default function Profile() {
const [name, setName] = useState("Sam");
useEffect(() => {
document.title = `Profile: ${name}`;
});
return (
<div>
<p>{name}</p>
<button onClick={() => 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.
JSX
import { useEffect } from "react";
export default function Welcome() {
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.
JSX
import { useEffect, useState } from "react";
export default function CounterTitle() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
Run after every render (rare)
Omit the array when you truly want the effect after every render.
JSX
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.
JSX
import { useEffect, useState } from "react";
export default function WindowWidth() {
const [width, setWidth] = useState(() => window.innerWidth);
useEffect(() => {
function onResize() {
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
JSX
import { useEffect, useState } from "react";
export default function TitleSync() {
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
JSX
import { useEffect, useState } from "react";
export default function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const id = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(id);
}, [running]);
return (
<div>
<p>{seconds}s</p>
<button onClick={() => setRunning(true)}>Start</button>
<button onClick={() => setRunning(false)}>Stop</button>
<button onClick={() => setSeconds(0)}>Reset</button>
</div>
);
}
Example 3: Subscribe to a browser event
JSX
import { useEffect, useState } from "react";
export default function OnlineStatus() {
const [online, setOnline] = useState(() => navigator.onLine);
useEffect(() => {
function onOnline() {
setOnline(true);
}
function onOffline() {
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:
JSX
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.
JSX
useEffect(() => {
setCount(c => c + 1);
}, []);
Mistake 2: Missing dependencies (stale values inside the effect)
What you might do:
JSX
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.
JSX
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