Skip to content
RungsySign in

Effects & Lifecycles

Synchronising with the outside world — and why you probably don't need an effect.

50 minDifficulty 3/5reactAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.

Before this

Why this exists

A component needs to fetch data when it mounts, subscribe to a WebSocket, or sync a value to `document.title` — none of these are things you compute WHILE rendering, they're things that need to happen AFTER the DOM is actually updated, as a reaction to it. `useEffect` is React's mechanism for exactly that category of work: synchronizing a component with something outside React's own rendering.

The mental model

Don't think of `useEffect` as "run this when X changes" — think of it as "keep this external thing synchronized with these specific reactive values." The dependency array isn't an event trigger list; it's a declaration of everything the effect actually reads, and React uses it to know when the synchronization needs to be redone.

How it works

The dependency array must list everything the effect reads

If an effect reads `userId` from the component's scope but `userId` is missing from the dependency array, the effect will keep using a STALE captured value of `userId` even after it changes on subsequent renders — the closure captured the old value and the effect never re-runs to pick up the new one. Omitting a dependency isn't an optimisation; it's usually a bug.

The cleanup function undoes exactly what the effect set up

An effect that subscribes to an event should return a cleanup function that unsubscribes — React calls this cleanup before running the effect again (when dependencies change) and when the component unmounts. Without it, every re-run of the effect adds a NEW subscription without removing the old one, leaking listeners and eventually firing callbacks multiple times for one event.

Most things people reach for an effect for don't need one

Computing a derived value from props or state (`fullName = firstName + ' ' + lastName`) doesn't need an effect and a piece of state to hold the result — just compute it directly during render. Effects are for genuine synchronization with something OUTSIDE React (a subscription, a DOM API, a network request), not for computing values React could just calculate as it renders.

Effects run after the browser paints, by default

`useEffect` runs asynchronously after React has committed changes to the DOM and the browser has had a chance to paint — this is intentional, so effects don't block the screen from updating. `useLayoutEffect` runs synchronously before paint instead, for the rare cases where you need to measure or adjust the DOM before the user sees anything, at the cost of potentially delaying that paint.

The mechanism

React renders and commits DOM changes, the browser paints, and only then does the effect run — keeping the paint fast and uninterrupted. If a later render's dependency array differs from the previous one, React runs this effect's cleanup function first, then runs the effect body again with the new values.

sequenceDiagram
  participant R as Render
  participant B as Browser paint
  participant E as Effect
  R->>B: commit DOM changes
  B->>B: paint screen
  B->>E: run effect (after paint)
  Note over E: dependency changed next render?
  E->>E: run cleanup, then re-run effect
Diagram source for Effects & Lifecycles.

What people get wrong

The dependency array is a list of things that should trigger the effect, so you can choose to leave things out to control when it runs.
The dependency array must honestly list everything the effect reads from component scope — it's not a trigger list to curate, it's what tells React the effect needs the LATEST value of each of those things. Deliberately omitting a dependency to 'control timing' produces a stale closure bug: the effect keeps using the OLD value of the omitted variable indefinitely, which usually isn't the intended behaviour at all.
useEffect is the right place to compute any derived value from state or props.
A value derivable directly from current props and state should just be computed during render, not stored in its own state and updated via an effect — that pattern causes an extra unnecessary re-render and a moment where the value is stale. Overusing effects for derived values is one of the most common sources of unnecessary complexity and extra re-renders in React codebases.
An effect without a cleanup function is fine as long as the component doesn't unmount.
Cleanup also runs before the effect re-runs due to a dependency change, not just on unmount — a missing cleanup for a subscription means EVERY dependency change adds a new subscription on top of the old one, even while the component stays mounted. This causes a specific, hard-to-spot bug where an event handler fires more and more times the longer a component's dependencies keep changing, because old subscriptions were never removed.

When not to use it

You need to measure a DOM element's size and adjust styles before the user sees any flicker.
`useLayoutEffect`, which runs synchronously before the browser paints, at the cost of potentially delaying that paint — appropriate specifically because the alternative is a visible flash of incorrect layout.
A value can be computed directly from current props and state with no external system involved.
Compute it inline during render, or memoize it with `useMemo` if the computation is expensive — not an effect plus a separate piece of state.

Terms

Dependency array
The array passed as useEffect's second argument, listing every reactive value the effect body reads, telling React when the effect needs to re-synchronize.
Cleanup function
The function an effect can return, which React runs before the effect re-runs and when the component unmounts, undoing whatever the effect set up.
Stale closure
A function (including an effect body) that captured an old value from a previous render and keeps using it, typically because a dependency was omitted.
useLayoutEffect
A variant of useEffect that runs synchronously after DOM mutations but before the browser paints, used for measurements or adjustments that must happen before the user sees anything.

In an interview

A useEffect that subscribes to a WebSocket doesn't include a cleanup function. What goes wrong, specifically, as the component re-renders with new dependencies?

  • each time the effect re-runs (dependency changed), a new subscription is added without the old one being removed
  • this leaks subscriptions, and eventually the same event triggers the callback multiple times, once per still-active old subscription
  • the fix is returning a cleanup function that unsubscribes, which React calls before each re-run and on unmount

Can you recall it?

Why must the dependency array honestly list every value the effect reads, rather than being used to selectively control when the effect runs?

Keep track of this

Add React Fundamentals to your map and Rungsy will schedule reviews so you actually remember it.