useEffect — Synchronising With the Outside World
IntermediateAn effect exists to synchronise your component with something outside React. It is not a lifecycle hook and not a place to put "code that runs after state changes".
Overview
useEffect is the most misused hook in React, and the reason is its name. People read it as "run this when something changes", which turns it into a general-purpose reaction mechanism and produces cascading renders, stale data and infinite loops. The accurate reading is narrower: an effect connects your component to an external system — a subscription, a browser API, a non-React widget, a network request — and cleans that connection up when it is no longer needed. If nothing outside React is involved, an effect is almost certainly the wrong tool.
The Shape of an Effect
A setup function, an optional cleanup, and a dependency array that lists everything the setup reads.
useEffect(() => {
// setup — runs after the browser has painted
const es = new EventSource(`/api/jobs/${jobId}/progress`)
es.onmessage = (e) => setProgress(JSON.parse(e.data))
return () => es.close() // cleanup — before the next run, and on unmount
}, [jobId]) // dependencies
// The dependency array is not a trigger list. It is a claim:
// "this setup reads exactly these values." React re-runs the effect
// when any of them changed since last time.
// The three forms
useEffect(fn) // after EVERY render — almost always a mistake
useEffect(fn, []) // once on mount, cleanup on unmount
useEffect(fn, [a, b]) // when a or b changed
// Effects run after paint, so the user sees the render first.
// useLayoutEffect runs before paint — use it only to measure or
// position something the user must never see in the wrong place.The Dependency Array
Lying to it causes stale bugs. Fixing it honestly usually means restructuring, not silencing the lint rule.
// The rule: every reactive value used inside must be listed —
// props, state, and anything derived from them.
useEffect(() => {
loadProblems(filter, page)
}, [filter, page]) // honest
useEffect(() => {
loadProblems(filter, page)
}, []) // lies — captures the FIRST filter forever
// eslint react-hooks/exhaustive-deps flags this. It is right.
// The real problem is usually an object or function dependency that
// is recreated every render, causing an infinite loop:
const options = { topic, limit: 20 } // new object each render
useEffect(() => { load(options) }, [options]) // runs forever
// Fixes, in order of preference:
// 1. Depend on primitives instead of the object
useEffect(() => { load({ topic, limit: 20 }) }, [topic])
// 2. Move the function inside the effect
// 3. useCallback / useMemo on the dependency — last resortWhat Actually Belongs Here
A short list. If your case is not on it, check the next concept before writing the effect.
// Legitimate effects — all of them touch something outside React:
// - subscribing to a WebSocket, EventSource or store
// - adding a window/document listener
// - setting up an IntersectionObserver or ResizeObserver
// - integrating a non-React library (a chart, a map, an editor)
// - setting document.title or reading from localStorage
// - fetching data, when you have no data library
useEffect(() => {
document.title = problem ? `${problem.title} · AiCanCode` : 'AiCanCode'
}, [problem])
useEffect(() => {
const onKey = (e) => e.key === 'Escape' && onClose()
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [onClose])
// Not effects: transforming data for display, responding to a user
// event, resetting state when a prop changes. Those have better tools.Key Points to Remember
- 1An effect synchronises a component with an external system — it is not a lifecycle or a reaction hook
- 2The dependency array declares what the setup reads; omitting a value captures a stale one forever
- 3An object or function recreated each render is the usual cause of an infinite effect loop — depend on primitives
- 4Effects run after paint; useLayoutEffect runs before paint and is only for measuring or positioning
- 5Never silence react-hooks/exhaustive-deps — restructure the effect so the honest list is correct
Interview Questions
Sign in to ask AriaWhat is useEffect actually for, and what is it not for?
Why does an effect with an object in its dependency array often loop forever?
What is the difference between useEffect and useLayoutEffect?
Ask Aria about useEffect — Synchronising With the Outside World
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.