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:
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:
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:
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:
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.
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.
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. Next up: Fetching data, where components pull in data that lives outside React entirely.

