Skip to content

Step 1 of 5 · Reading · ~4 min

Read

Hooks & State

The useEffect Hook

Render must be pure: it may run, be thrown away and run again. Anything that reaches outside the component (a request, a timer, a subscription, direct DOM work) belongs in an effect, which runs after the commit. After this lesson your program plays a component's effects through a series of renders and an unmount, and prints exactly which cleanups and runs happen, in React's order.

The dependency array

Each useEffect call carries a list of the values the effect reads.

DepsThe effect runs
[id, tab]after the first render, and after any render where id or tab changed
[]once, after the first render
omittedafter every render. Almost never what you want

Lists are compared item by item with Object.is, in order, so [a, b] and [b, a] differ, and a fresh object or arrow function in the list differs on every render, which makes the effect run every time. A list of a different length also counts as changed.

Cleanup

An effect may return a function. It runs before the next run of that same effect, and once more when the component unmounts. Without it the leak is easy to picture: change roomId from 1 to 2 with no cleanup and you now hold two open sockets, both writing into the same component. The other half people forget is that cleanup also runs on every re-run, not only on unmount.

The order, which is the part to get exactly right

When a render commits, React decides for every effect whether its deps changed. Then it does two passes over the effects, not one:

  1. run the cleanups of all effects that will run again, in the order they were written;
  2. run all of those effects, in the order they were written.

It does not interleave "cleanup A, run A, cleanup B, run B". Two effects that touch the same resource therefore see every old subscription removed before any new one is made.

Worked trace. A component has two effects, room with deps [roomId] and title with deps [name]:

render 1  (roomId 1, name Ada)    run room, run title                 both are new
render 2  (roomId 1, name Bob)    cleanup title, run title            only title's deps changed
render 3  (roomId 2, name Bob)    cleanup room, run room              only room's deps changed
unmount                           cleanup room, cleanup title         declaration order

On the very first render there is nothing to clean up. On unmount every effect cleans up and nothing runs. After an unmount, a new mount starts from scratch, as if for the first time.

What the runtime stores

Each effect call owns a slot like the ones from useState: it keeps the last deps and the last cleanup. The call compares its new deps with the stored ones. If they differ, or there is no list, it queues the effect for after the commit and remembers the new deps. The comparison is the part you write; the scheduling after commit is what lets effects measure and subscribe to a DOM that is real.

Traps

Leaving a value out of the deps freezes it at the render that created the closure, the stale-closure bug again. Putting an unstable value in re-runs the effect constantly. Two overlapping requests can resolve out of order, so a slow answer for the old id overwrites the fast one for the new id; the cleanup is where you cancel or ignore the first. In development, strict mode mounts, cleans up and mounts again to expose effects that forgot their cleanup. Newer APIs reduce the need for effects (useSyncExternalStore for external state, Suspense and Server Components for data loading), but commit-then-effect is unchanged underneath.

The script you will run

RENDER room=[1] title=[Ada] tick=*     one render: each effect with its dependency list
UNMOUNT

* means no list at all. The exact output lines are in the exercise.

Common mistakes

  • Interleaving cleanup and run per effect instead of all cleanups, then all runs.
  • Running a [] effect again on later renders.
  • Calling a cleanup on the first render, when nothing has run yet.
  • Treating the list as a set, so [a,b] and [b,a] look equal.
  • Forgetting that an omitted list runs every time, including its cleanup.

Your exercise

The starter parses the script and prints a render k line for each render. You write render_effects, which decides the cleanup and run lines for one render, and unmount_effects.

Up nextSignals & Fine-Grained ReactivityHooks & State

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…