Skip to content

Effects

Imagine a component that needs to keep a timer ticking after it renders, the way the Timer component below does. Most of what a component does happens while it renders: it reads props and state and returns JSX. Keeping a timer running is different. So is opening a subscription, syncing the document title, or fetching data from a server. Each one reaches outside that world to touch something React doesn't manage. useEffect is the hook for that. It lets a component run some code after it renders, so it can sync itself with that outside thing.

Here's an effect that starts the timer once and ticks a counter every second:

jsx
import { useState, useEffect } from 'react'

function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => setCount(c => c + 1), 1000)
    return () => clearInterval(id)
  }, [])

  return <p>{count}</p>
}

Three parts are doing the work here, and they map onto the three things every effect deals with: what to do, how to clean it up, and when to run.

The function you pass to useEffect is the effect itself. This one calls setInterval to start a repeating timer that bumps count up by one each second.

The dependency array

The second argument, the [] at the end, is the dependency array. It controls when the effect runs.

  • An empty array [] means the effect runs once, after the component first appears on the screen. The timer above uses this: start the interval one time and leave it running.
  • An array with values in it, like [a, b], means the effect runs after the first render and again any time one of those values changes between renders. This is how you re-sync when something the effect depends on has changed.
  • Leaving the array out entirely means the effect runs after every single render. That is rarely what you want, and it's a common source of runaway loops when the effect also updates state.

So the array is your answer to the question "when should this code run again?" List the values the effect reads and depends on, and React re-runs it when any of them change.

Cleanup functions

The effect above returns a function:

jsx
return () => clearInterval(id)

That returned function is the cleanup function. React runs it before the effect runs again, and once more when the component is removed from the screen. Its job is to undo whatever the effect set up.

Cleanup matters because effects often start something that keeps going on its own: an interval, an event listener, an open connection. If the component is removed and nothing stops that interval, it keeps firing forever and keeps a reference to a component that's gone. Do that a few times and you have a slow leak and updates landing on things that no longer exist. The rule of thumb: if an effect starts something, subscribes to something, or opens something, its cleanup function should stop, unsubscribe, or close it.

You might not need an effect

Effects are for syncing with the outside world, so a lot of code that looks like a candidate for useEffect belongs somewhere else. Two cases come up constantly.

The first is derived values. If you can calculate something from props and state you already have, calculate it right there during render:

jsx
function Cart({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0)
  return <p>Total: {total}</p>
}

total is derived from items, so it gets computed on every render and is always in sync. Storing it in its own state and updating it from an effect would be more code and an extra render for no gain.

The second is responding to a user action. When something should happen because the user clicked or typed, that code goes in the event handler for that action:

jsx
function BuyButton({ product }) {
  function handleClick() {
    buyProduct(product)
  }
  return <button onClick={handleClick}>Buy</button>
}

The purchase happens in handleClick, right where the click is handled. Routing it through an effect would add a layer of indirection and make the flow harder to follow. A good test: if the code runs because of a specific interaction, reach for an event handler; if it runs to keep the component in sync with an external system, reach for an effect.

The practical question with any effect is what belongs in the dependency array. The answer is mechanical: every reactive value the effect reads, meaning every prop, state variable, or value derived from them that appears inside the effect function, belongs in the array. Leave one out and the effect keeps using a stale copy of it instead of picking up changes.

jsx
function SearchResults({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    let active = true
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => { if (active) setResults(data) })
    return () => { active = false }
  }, [query])

  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
}

This effect reads query, so query is in the array. Nothing else in scope changes what the effect does, so nothing else needs to be there. You don't have to work this out by memory. The eslint-plugin-react-hooks lint rule, exhaustive-deps, reads the effect function and flags any value it uses that's missing from the array. Treat its warnings as real bugs to fix, not noise to silence, since a suppressed warning here is exactly how stale-value bugs slip into production.

When an effect starts doing two unrelated jobs, split it into two effects instead of one effect with a longer dependency list. A component that both fetches a user's data and sets the document title based on that user is easier to reason about as two separate useEffect calls, each with its own focused dependency array, than as one effect juggling both concerns. Each effect should sync one thing with the outside world.

A quick test for whether code belongs in an effect at all: does it fetch something, subscribe to something, or reach out and touch the DOM or some other system outside React directly? That's effect territory. Does it compute a value from props or state you already have? That belongs in the render body, not an effect. Reaching for useEffect to derive a value is the most common way effects get overused.

Effect timing is worth being precise about. Your effect does not run during render. React renders the component, commits the DOM update, the browser paints, and only then does the effect run. This is deliberate: the effect sees a screen that already reflects the current render, and it never blocks the browser from painting. It also means you can't rely on an effect to produce something the user sees before the first paint. For the rarer case where you need to measure or mutate the DOM before paint, there's useLayoutEffect, which runs synchronously after commit and before the browser paints.

You'll notice in development that effects run twice on mount. React's Strict Mode intentionally mounts each component, unmounts it, and mounts it again. It runs your effect, runs the cleanup, then runs the effect a second time. This is a stress test for cleanup. If your effect sets something up but never tears it down, the double run surfaces the bug right away, because you'll see two intervals or two subscriptions instead of one. This only happens in development. In production the effect runs once.

Two dependency-array footguns are worth internalizing. The first is stale closures. An effect captures the values from the render it was created in. If you leave a value out of the dependency array, the effect keeps reading the old value from that render even after it has changed, and it never re-runs to pick up the new one. The fix is to list every value the effect reads, or to use the updater form like setCount(c => c + 1) so you don't need to read the current value at all. The second is identity. Objects, arrays, and functions created during render are fresh values every time, so listing one as a dependency makes the effect re-run on every render, since the reference is never equal to last time's. When you need one as a dependency, define it inside the effect, or wrap it in useMemo or useCallback (hooks that cache a value or function between renders) so its identity stays stable between renders.

Version note

In class components this behavior was split across three lifecycle methods: componentDidMount for setup after the first render, componentDidUpdate for re-running when data changed, and componentWillUnmount for cleanup. A single useEffect with a dependency array and a cleanup function covers all three, which is why related setup and teardown code now lives together instead of being spread across separate methods.

JunoEffects sync with the outside world Think of useEffect as the place for code that reaches outside React: a timer, the document title, a request to a server. The array at the end says when to run it, [] meaning once, right after the component shows up. And if your effect starts something, return a small function that stops it. A lot of the time you won't need an effect at all, so it's a good habit to pause and ask whether a plain calculation or a click handler would do the job first.
JunoEffects sync with the outside worlduseEffect runs after render to sync with something React doesn't own. The dependency array is the control: [] runs once, [a, b] re-runs when those change, and no array runs every render. Return a cleanup function for anything you start, subscribe to, or open. Before you write one, check whether the value can be derived during render or the work belongs in an event handler, because those cover a surprising amount of what looks like effect territory.
JunoEffects sync with the outside world Effects run after commit, so they see a painted screen and never block it; reach for useLayoutEffect only when you must touch the DOM before paint. Strict Mode's double mount in development is a cleanup test, so treat any doubled interval or subscription as a real bug. The two dependency traps are stale closures from omitted values and unstable object or function identities that re-run the effect every render; the updater form, useMemo, and useCallback are how you defuse them.

Next up: Fetching data, where components pull in data that lives outside React entirely.