Effects and useRef
When you need useEffect, cleanup, and when you DON'T need it.
Updated
What an effect is
A component must be pure — it only computes JSX. But sometimes you need to synchronize the component with something outside React: a timer, a WebSocket connection, a browser API, a non-React library. That's what useEffect is for: code that runs after React has updated the screen.
useEffect(() => {
const id = setInterval(tick, 1000) // you start the synchronization
return () => clearInterval(id) // cleanup: you stop it
}, []) // dependenciesAn effect's lifecycle
- The component renders and appears on screen.
- The effect runs.
- When the dependencies change: React runs the old cleanup, then the effect again.
- On unmount: the cleanup runs.
Dependencies
| The second argument | The effect runs |
|---|---|
| missing | after every render |
[] |
once, after mounting |
[a, b] |
after mounting and whenever a or b changes (compared with Object.is) |
You put in the list everything the effect uses from props and state. The ESLint rule react-hooks/exhaustive-deps helps you.
You might not need an effect
The most common mistake in React: effects for things that aren't synchronization with the outside world.
| Instead of an effect to... | Do this |
|---|---|
| compute data from state / props | compute it directly while rendering |
| react to a click / submit | put the logic in the event handler |
| reset state when a prop changes | key={id} on the component |
| fetch data from the server | a Server Component or TanStack Query |
| tell the parent something changed | call the callback in the handler that made the change |
The key question: "Does this code run because the component appeared on screen, or because the user did something?" The first → an effect. The second → an event handler.
Fetching in an effect — if you really have to
useEffect(() => {
const controller = new AbortController()
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(() => {})
return () => controller.abort() // cancels the old request if id changes quickly
}, [id])Without the cleanup you have a race condition: the old response can arrive after the new one and overwrite it.
useRef — memory without re-rendering
useState |
useRef |
|
|---|---|---|
| A change triggers a render | yes | no |
| Read during rendering | yes | no (only in effects / handlers) |
| For | what appears on screen | DOM references, timer ids, "behind the scenes" values |
const inputRef = useRef<HTMLInputElement>(null)
<input ref={inputRef} />
inputRef.current?.focus()Strict Mode
In development, React mounts → unmounts → mounts again every component, to check that effects have a correct cleanup. If something "runs twice" and breaks, the effect is missing its cleanup. In production it runs only once.
Summary
- An effect = synchronization with something outside React; cleanup is mandatory when you start something.
- The dependencies = everything the effect uses.
- Calculations → during rendering; reactions to actions → handlers; data → Server Components / TanStack Query.