React from the ground up

Effects: talking to the outside

useEffect and the one job it does — synchronising with things outside React — with the dependency array, the cleanup, and the mistakes the two prevent.

Part 10 of 12

Intermediate17 min readBack to Courses

The one thing render must not do

Rendering is a description, so it must not act: no timers started, no title changed, no network called, no subscription opened. Those actions belong to useEffect, which runs after React has put the picture on screen — the place where the picture and the outside world are brought into step.

The effect is a function, passed to useEffect, that runs after the render commits. Inside it, the component is allowed to be impure: set up a timer, talk to the DOM, drive a title. This chapter's examples are the two everyday cases: syncing the document title with state, and a clock that ticks.

The dependency array, and cleanup

By default an effect runs after every render, which is rarely what is wanted. The second argument is the list of what the effect depends on: useEffect(() => { … }, [name]) runs after the first render and again whenever name changes. An empty array — [] — means "nothing it depends on": run it once, after mount.

Anything the effect starts, it must be able to stop. Return a function and React calls it before the next run of the effect and when the component goes away — that is where a timer is cleared, a subscription cancelled, an event listener removed. Without it, each run leaves the previous one alive: two clocks, three listeners, a page that gets slower the longer you stay.

What an effect is not for

An effect is not for computing the page from the data — that is what render does, and a value derived in render is never stale. The doubled count from the state chapter does not need an effect, and a second state that mirrors the first is the sign one was used where a calculation belonged.

The hard case is the honest one: if nothing outside the component is involved, there is no effect to write. Effects are for the outside — and the checklist is short: what does this synchronise, what does it depend on, and what must it undo?

Cleanup runs before the next setup

Cleanup is not only for unmounting. React calls the returned function before the effect runs again, so each setup starts from a clean slate: the old timer is gone, the old listener is removed, and the new one is the only one alive. A component that subscribes twice and unsubscribes once is the symptom of a missing return.

A title that follows the state, and a clock that stops

const { useState, useEffect } = React;

function Clock() {
  const [now, setNow] = useState(() => new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);

    // Cleanup: called before the next effect and when Clock is removed.
    return () => clearInterval(id);
  }, []); // no dependency: set up once, after the first render

  return <p>The time is {now.toLocaleTimeString()}.</p>;
}

function App() {
  const [name, setName] = useState('');

  useEffect(() => {
    // The tab title is outside React; an effect keeps it in step.
    document.title = name === '' ? 'Untitled' : name;
  }, [name]);

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Clock />
    </div>
  );
}

After the commit, not during it

React first calls your components and builds the description, then updates the DOM — that part is the commit — and only then runs effects. An effect can therefore read the page as the reader will see it, which is exactly what a title change, a measurement or a subscription needs.

When an effect sets state, React schedules another render after the effect finishes. That is legal and sometimes necessary, but it is a second pass: if the value can be computed from the current props and state, compute it during render instead.

Dependencies are compared, not guessed

React compares each dependency with the value from the previous run using Object.is. A number or string changes when it changes; an object or array literal is new on every render, so [user] with an inline object will run the effect every time. Depend on the fields you actually read, not on a fresh wrapper.

An empty array is a promise that nothing outside changes. If the effect reads a prop or a state value and the array omits it, the effect keeps the old value — the stale closure that makes a timer or listener behave as if the page never moved on.

Three effect mistakes: an empty array around an effect that reads changing state; an object or array dependency that is recreated every render, so the effect runs in a loop; and starting a subscription or timer without returning the cleanup, which leaves one more copy alive after every run.

Try these next

  • Add a button that changes the step and press an arrow key twice; confirm the new step is used.
  • Remove the cleanup, change the step a few times, and count how many times one key press moves the total.
  • Replace the key listener with a click listener on the window and clean it up the same way.

A timer, a title, and a cleanup

Run it and watch the clock tick while the page title follows what you type. Then delete the `return () => clearInterval(id)` line, run again, and check the console — the effect now leaks a timer on every rebuild.

Live Preview
Console output will appear here...

A key listener that cleans up

Click inside the preview, then press the up and down arrows. Change the step and press again: the listener is re-subscribed with the new value. Then remove the cleanup and repeat — one press starts moving the total more than once.

Live Preview
Console output will appear here...

An effect that sets state it also depends on is a loop: state changes, effect runs, sets state again. If you spot one, the state being set is almost always something that should have been computed during render.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is an effect for?