React useEffect Explained: When to Use It (and When Not To)
useEffect runs code after React renders, to sync your component with something outside React — a timer, a subscription, a browser API. How the dependency array works, cleanup functions, why effects run twice in development, and the many cases where you don't need an effect at all.
useEffect is the React hook people misuse most — AI tools included. The official React docs even have a page called "You Might Not Need an Effect." Here's what it's actually for, how it works, and when to reach for something else.
(New to React? Start with What is React? and useState explained.)
What it's for
React's job is to turn data into what's on screen. Most of your component is just that: take props and state, return JSX.
Sometimes a component also needs to talk to something outside React: start a timer, listen to window resizing, connect to a WebSocket, update the page title, use a non-React library. That's what useEffect is for: synchronising your component with an external system.
useEffect(() => {
document.title = `${count} unread messages`
}, [count])
"After rendering, whenever count changes, update the browser tab's title."
The dependency array
The second argument controls when the effect runs:
| You write | Effect runs |
|---|---|
useEffect(fn) |
After every render |
useEffect(fn, []) |
Once, after the first render |
useEffect(fn, [a, b]) |
After the first render, and whenever a or b changes |
The rule: list every value from the component that the effect uses. The react-hooks/exhaustive-deps lint rule checks this for you. Leaving values out to "make it run less" causes bugs where the effect sees stale data.
Cleanup
If your effect starts something, it should stop it. Return a function:
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000)
return () => clearInterval(id) // cleanup
}, [])
React runs the cleanup before the effect runs again, and when the component disappears. Without it, you get duplicated timers, memory leaks and "can't update unmounted component" bugs. (Node memory leaks are the server-side cousin.)
"Why does my effect run twice?"
In development with <StrictMode>, React deliberately mounts, unmounts and remounts each component once, so effects run → clean up → run again. It's a test: if your effect breaks when run twice, it's missing cleanup. This doesn't happen in production. Don't remove StrictMode to hide it; fix the cleanup.
When you don't need an effect
Most bad effects fall into these patterns:
1. Computing something from props or state.
// ❌
const [fullName, setFullName] = useState('')
useEffect(() => setFullName(`${first} ${last}`), [first, last])
// ✅ just compute it
const fullName = `${first} ${last}`
2. Responding to a user action. Put the code in the event handler, not in an effect that watches state.
// ✅
function handleSubmit() {
saveOrder(order)
showToast('Saved')
}
3. Resetting state when a prop changes. Use a key on the component instead.
These unnecessary effects cause extra renders and, often, infinite loops.
Fetching data in an effect
You can fetch in an effect, and you'll see it everywhere:
useEffect(() => {
let ignore = false
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(data => { if (!ignore) setUser(data) })
return () => { ignore = true }
}, [id])
Note the ignore flag: without it, a slow old request can overwrite the result of a newer one. You also need loading and error state, caching, and retry. That's why most apps use a data library — TanStack Query, SWR — or their framework's data loading (Next.js server components) instead. (TanStack Query vs useEffect)
The summary
useEffectsyncs a component with something outside React.- The dependency array decides when it runs; list everything the effect uses.
- Return a cleanup function for anything you start.
- Double-running in development is a deliberate check.
- Derived values, event responses and state resets don't need effects.
EasySpawn gives Claude Code a persistent server with your dev server running, so you can ask it to remove unnecessary effects and see the result in a live preview. See how it works or join the waitlist.
Related: Too Many Re-renders · useState Explained · Async/Await in JavaScript · What Is React?
Keep reading
What Is TypeScript? JavaScript With Labels, Explained for Beginners
TypeScript is JavaScript plus types: labels that say what kind of value each thing holds, checked before your code runs. What it looks like, why AI tools default to it, what .ts and .tsx files are, and how to read the red squiggles.
What Is a Headless CMS? And Does Your App Need One?
A headless CMS is a content editor with no website attached — it stores your content and hands it to your app through an API. How it differs from WordPress, the popular options, when it beats Markdown files or your own database, and when it's overkill.