Effects

Reach outside React when you genuinely have to — and recognise the many times you do not.

11of 20
≈ 14Min read
8Parts

What an effect is for

Rendering is a calculation: props and state in, elements out, nothing else touched. An effect is the escape hatch for the "nothing else" — synchronising your component with something outside React: a subscription, a timer, the document title, a media player, a network request.

the shape
React.useEffect(() => {
  // runs AFTER the render is on the screen
  const id = setInterval(tick, 1000);

  return () => clearInterval(id);   // cleanup
}, [delay]);                        // dependencies

The dependency array

Written asRuns
useEffect(fn)After every render — almost always a mistake
useEffect(fn, [])Once after the first render, cleanup on unmount
useEffect(fn, [a, b])When a or b changes (compared with Object.is)
The array is not a schedule

It is a list of everything the effect reads. Leaving something out to make the effect run less often is how you get stale values that nobody can reproduce. If the effect runs too often, the answer is to change what it depends on — not to lie about it.

Cleanup is not optional

React runs the cleanup before re-running the effect, and again when the component unmounts. Without it you get two intervals, then four; listeners on elements that no longer exist; and setState on something that has gone.

In development StrictMode mounts, cleans up and mounts again on purpose — so a missing cleanup breaks loudly and immediately, instead of quietly in production.

every subscription, timer and listener
React.useEffect(() => {
  function onResize() { setWidth(window.innerWidth); }
  window.addEventListener("resize", onResize);
  return () => window.removeEventListener("resize", onResize);
}, []);

You probably do not need an effect

the most common one in the wild
// no
const [shown, setShown] = React.useState([]);
React.useEffect(() => { setShown(doors.filter(f)); }, [doors, query]);
// renders once with the old list, then again with the new one,
// and goes wrong the moment the two disagree

// yes
const shown = doors.filter(f);
Instead of an effect that…Do this
Computes a value from props or stateCompute it during render
Resets state when a prop changesChange the component key
Runs because the user clickedPut it in the event handler
Tells the parent that state changedCall the parent in the same handler that changed it
Sets state from other stateDelete the second state

An effect that depends on a value

Read it as a statement about synchronisation, not as a lifecycle: "while this component is on screen with this roomId, a connection to that room exists". The cleanup is how that sentence stays true.

resubscribe when the thing being watched changes
React.useEffect(() => {
  const socket = connect(roomId);
  socket.on("message", add);

  return () => socket.close();
}, [roomId]);

// roomId "a" → "b" gives, in order:
// close(a) → connect(b)

Objects and functions as dependencies

Prefer primitives in the array. If a function is genuinely needed, define it inside the effect, or wrap it in useCallback where it is created.

a new object every render is a new dependency every render
// this effect runs on EVERY render: options is a fresh object each time
const options = { room: roomId };
React.useEffect(() => { connect(options); }, [options]);

// move it inside — now the dependency is the string
React.useEffect(() => {
  connect({ room: roomId });
}, [roomId]);

The three timings

  • useEffect — after the browser has painted. The default, and what you want for anything that can flash for a frame without harm.
  • useLayoutEffect — after the DOM is updated but before paint. For measuring a node and adjusting position in the same frame, so nothing is ever seen wrong. It blocks paint, so use it only for that.
  • useInsertionEffect — for CSS-in-JS libraries inserting rules. You will not write one.

Async inside

the effect function itself can never be async
// wrong: an async function returns a promise, and React expects a cleanup
React.useEffect(async () => { … }, []);

// right
React.useEffect(() => {
  let alive = true;
  (async () => {
    const data = await load();
    if (alive) setData(data);
  })();
  return () => { alive = false; };
}, []);

Practice