Skip to the chapter
←ReactGGroundwork⚛React
React/Intermediate/I5
16 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛I5 · Part 2 · Intermediate

Effects in depth

Stale closures, the dependency array as a contract, and the effects that should not exist.

5 min read9 sections4 exercises
16 of 57

In this chapter

  • The stale closure
  • useEffectEvent, properly
  • The dependency array is a contract, not a lint rule
  • Removing dependencies honestly
  • One effect per concern

and 4 more

about 5 min
09On this page
  1. The stale closure
  2. useEffectEvent, properly
  3. The dependency array is a contract, not a lint rule
  4. Removing dependencies honestly
  5. One effect per concern
  6. The taxonomy: what is really an effect
  7. Adjusting state during render
  8. Race conditions, properly
  9. Effects and Strict Mode

The stale closure

The single most common effect bug, and once you see the mechanism you can spot it anywhere.

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);        // count is frozen at 0, forever
  }, 1000);
  return () => clearInterval(id);
}, []);                          // runs once — captures the first render

The effect ran during the first render, so the arrow inside it closed over that render's count — zero. It never re-runs, so it never sees another one. The counter goes 0, 1, 1, 1, 1.

Four fixes, in order of preference:

// 1. don't read it at all — the updater form gets the latest value
setCount((c) => c + 1);

// 2. depend on it, and accept the interval restarting
}, [count]);

// 3. useEffectEvent — read the latest value without depending on it
const onTick = useEffectEvent(() => setCount(count + increment));
useEffect(() => {
  const id = setInterval(onTick, 1000);
  return () => clearInterval(id);
}, []);                          // no dependencies, no stale values

// 4. the ref workaround this replaces — still everywhere in existing code
const latest = useRef(count);
useEffect(() => { latest.current = count; });     // keep it current after every render

useEffect(() => {
  const id = setInterval(() => {
    setCount(latest.current + 1);                  // the interval reads the ref, not the closure
  }, 1000);
  return () => clearInterval(id);
}, []);                                            // still no dependencies, and no stale value

useEffectEvent, properly

Stable since React 19.2, and it is the sanctioned answer to the problem the ref workaround was invented for. An Effect Event is a function that always sees the latest props and state, but has a stable identity — so an effect can call it without listing it as a dependency.

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showToast("Connected!", theme);        // always the current theme
  });

  useEffect(() => {
    const conn = connect(roomId);
    conn.on("connected", onConnected);
    return () => conn.disconnect();
  }, [roomId]);                            // theme is NOT a dependency
}

Without it, adding theme to the dependencies reconnects the chat every time the user switches to dark mode; leaving it out shows the toast in the wrong theme. Neither is right, and that is the gap the hook closes.

The line it draws

An effect's dependencies should be the things it synchronises with. Values it merely reads when something happens are not synchronisation — they are event logic that happens to live inside an effect. That is the distinction the hook makes explicit, and it is why the name is "Effect Event" rather than "latest ref".

Two constraints. An Effect Event can only be called from inside an effect in the same component — never passed to a child, never called during render. And it is not a general escape from the dependency array: if you find yourself wrapping the whole effect body in one, the effect is synchronising with nothing and should be an event handler instead.

The dependency array is a contract, not a lint rule

It says: this effect stays in sync with these values. Removing a dependency to stop an effect re-running does not fix anything — it freezes the effect on an old value and turns a visible loop into a silent wrong answer.

If satisfying the linter breaks the effect

The effect is modelling the wrong thing. Something in it is not synchronisation — it is a reaction to an event, or a derived value, or an initialisation that should happen once elsewhere. Fix the shape rather than silencing the rule.

Removing dependencies honestly

// object dependency → depend on the field
}, [user]);            →   }, [user.id]);

// function dependency → move it inside the effect
useEffect(() => {
  function connect() { ... }        // now nothing external to depend on
  connect();
}, [roomId]);

// or hoist it out of the component entirely
function makeUrl(id) { ... }        // module scope: never changes

Each of these removes a dependency by making it genuinely unnecessary, rather than by lying about it.

One effect per concern

// ✗ two unrelated jobs, one dependency list
useEffect(() => {
  document.title = title;
  const id = setInterval(poll, 5000);
  return () => clearInterval(id);
}, [title]);                        // the interval restarts when the title changes

// ✓
useEffect(() => { document.title = title; }, [title]);
useEffect(() => {
  const id = setInterval(poll, 5000);
  return () => clearInterval(id);
}, []);

Effects are cheap. Splitting them is almost always right, because each one then has the dependency list it actually needs.

The taxonomy: what is really an effect

You want to…Use
Compute something from props or stateNothing — calculate it during render
React to a click, submit or keypressThe event handler
Reset state when a prop changesA key on the component
Adjust some state when a prop changesSet it during render, guarded by a comparison
Cache an expensive calculationuseMemo
Subscribe to an external storeuseSyncExternalStore
Fetch dataA data library, or the framework's loader
Connect to a socket, a timer, a browser APIan effect
Measure the DOM and adjust before paintuseLayoutEffect

Two rows on that list are effects. That ratio is roughly right for real codebases too.

Adjusting state during render

The unusual one from that table. When some state must change because a prop changed, an effect renders once with the wrong value first. React supports setting state during render instead:

function List({ items }) {
  const [selection, setSelection] = useState(null);
  const [prevItems, setPrevItems] = useState(items);

  if (items !== prevItems) {          // during render, guarded
    setPrevItems(items);
    setSelection(null);
  }
  ...
}

React sees the state change before it commits, discards the in-progress output and re-runs the component immediately — nothing is painted with the stale value. The guard is essential; without it this is an infinite loop.

Prefer a key when you want to reset everything. This is for resetting one field while keeping the rest.

Race conditions, properly

useEffect(() => {
  const controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then((r) => r.json())
    .then(setData)
    .catch((err) => {
      if (err.name !== "AbortError") setError(err);   // ignore our own cancel
    });

  return () => controller.abort();
}, [url]);

AbortController beats the cancelled flag because it also stops the request in flight, freeing the connection. Both fix the same bug: an older response arriving after a newer one and overwriting it.

Effects and Strict Mode

In development React mounts, unmounts and remounts every component once. An effect that is not idempotent shows up immediately: two websocket connections, two subscriptions, a doubled analytics event. The fix is always the cleanup, and the same bug would appear in production the first time a user navigated away and back.

Interview answer, one sentence

"Most effect bugs are one of two things: a stale closure, because the effect captured a render it never re-ran for, or a dependency someone deleted to stop it looping. The array is a statement about what the effect stays in sync with — and the deeper skill is recognising that derived values, event reactions and prop-driven resets are not effects at all. The React Compiler does not change this: it memoises values, but it will not tell you an effect was unnecessary, and its lint rules flag setting state in an effect for exactly that reason."

04Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsModel useEffect's contractintermediate
  • 8 testsStop the effect that never stopsintermediate
  • 7 testsA ticker that actually ticksintermediate
  • 6 testsUnsubscribe from the old room before subscribing to the new oneintermediate
All 66 for this level →
I5

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. I4Previous · I4Custom hooks
  2. I5you are here
  3. Next · I6React RouterThe URL is state your users can bookmark, share and go back to.6 min read

01On this page

9 sections · 0%

  1. 01The stale closure
  2. 02useEffectEvent, properly
  3. 03The dependency array is a contract, not a lint rule
  4. 04Removing dependencies honestly
  5. 05One effect per concern
  6. 06The taxonomy: what is really an effect
  7. 07Adjusting state during render
  8. 08Race conditions, properly
  9. 09Effects and Strict Mode

[] chapters · t top