Blog
3 min read

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

  • useEffect syncs 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