# useEffect — Part 2: Timing, StrictMode, and Internals

## 6. Effect Timing Relative to Render and Paint

Rendering, committing, and running effects are three distinct phases, and they do not happen back-to-back synchronously:

1. **Render** — React calls your component function, producing a description of the desired UI. No DOM is touched yet.
2. **Commit** — React applies the minimal set of DOM mutations needed to match that description.
3. **Browser paint** — the browser draws the updated DOM to the screen. The user sees the update here.
4. **Effects run** — only *after* the paint, and asynchronously (React schedules them, it doesn't run them inline as part of the commit).

```tsx
function Example() {
  console.log("1: render");
  useEffect(() => {
    console.log("3: effect (after paint)");
  });
  return null;
}
// Commit + paint happens between steps 1 and 3, invisible in this log,
// but it is a real step: the user's screen updates BEFORE the effect runs.
```

![Render, commit, and paint all complete before a useEffect callback ever runs — the effect is scheduled asynchronously afterward](diagrams/01-effect-timing.svg)

This ordering is exactly why `useEffect` is the wrong tool for anything the user must never see flash incorrectly (e.g. measuring a DOM node and immediately repositioning it) — the paint already happened by the time the effect runs. `useLayoutEffect` (its own topic) exists specifically for that case, running synchronously before paint instead.

## 7. Multiple Effects and Their Order

A component can call `useEffect` any number of times. Each call gets its own node on the fiber's hook linked list (see useState's "Under the Hood" for the linked-list mechanism itself), and effects run in the same order they were declared:

```tsx
function Example() {
  useEffect(() => console.log("effect A"), []);
  useEffect(() => console.log("effect B"), []);
  // on mount, logs "effect A" then "effect B", always in this order
}
```

Cleanups run in that same relative order too, but in reverse when a component unmounts entirely — the last-mounted effect's cleanup fires first, mirroring how nested resources are usually torn down (most-recently-acquired, first released).

## 8. StrictMode's Dev-Only Double Invoke

In development, wrapping a subtree in `<React.StrictMode>` makes React run each component's mount sequence twice: **mount → run effects → clean up those effects → mount again → run effects again.** This is dev-only (StrictMode has no effect in production builds) and exists specifically to surface effects with missing or incorrect cleanup — a subscription that isn't properly undone will visibly leak (double-fire, double-subscribe) under this pattern, whereas without StrictMode the bug is silent until a real remount happens in production, often much later and harder to trace.

```tsx
useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);
// Under StrictMode: setInterval → clearInterval → setInterval again.
// If the cleanup were missing, TWO intervals would end up running — a bug
// that's invisible without StrictMode until an actual unmount/remount occurs.
```

The fix for a component that "behaves wrong under StrictMode" is essentially never to remove StrictMode — it's to write cleanup that actually cleans up.

## Under the Hood

React does not attach state to the component *function* — a function component is just a function, called fresh every render with no persistent `this` and no memory of its own. Hook data has to live somewhere else: it lives on the **fiber**, React's internal object representing one component instance in the tree.

Each fiber holds a `memoizedState` pointer to the head of a **singly linked list of hook objects**, one node per hook call, built in the exact order those hooks are called. Every render, React walks this list from the head, one node per hook call, in lockstep with your function body executing top to bottom. Matching is done purely by **call order** — not by variable name, not by any key. This is the entire reason the Rules of Hooks forbid conditional hook calls: wrap a hook in an `if` and, on some render, every hook call after it shifts one slot in the walk — silently reading and writing the wrong hook's stored data.

`useEffect`'s own hook node stores two things from its last completed run: (1) the dependency array, and (2) the cleanup function it returned (or `undefined` if it returned nothing). On each render, React does **not** run the effect immediately — it compares the new dependency array against the stored one, element by element, using `Object.is` specifically (not loose `===`, and not a deep-equality check). This is a meaningful distinction at the edges: `Object.is(NaN, NaN)` is `true`, so a `NaN` dependency correctly compares as "unchanged," while `Object.is(0, -0)` is `false`, so `+0` and `-0` compare as "changed" even though `0 === -0` is `true` — a rare but real edge case worth knowing exists. If every element compares equal (or the array reference itself is stable, as with a literal `[]`, which never has any elements to differ), React skips scheduling the effect entirely for this render. If anything differs, React schedules the effect onto the fiber's effect list, tagged for the **passive effects phase** — meaning the browser is allowed to paint first, and only then does React flush passive effects asynchronously, batched together across the whole tree, so a slow effect never blocks the visual update the user is waiting on.

Before running the *new* effect function, React first invokes the *stored cleanup function from the previous run*, if one exists — which is why a cleanup closure always captures the previous render's props/state, never the current one; it's cleaning up exactly what that previous run set up, nothing more. `useLayoutEffect` (its own topic) uses this exact same hook-node structure and the exact same `Object.is` dependency-diffing — the only difference is which phase it's tagged for: layout effects run synchronously, after the DOM mutation but *before* the browser paints, blocking the paint until they finish; passive effects (`useEffect`) run after paint, non-blocking. Same mechanism, different scheduling phase, different consequences for what each is safe to use for.

## Common Pitfalls

1. **Missing a dependency** — closing over a prop/state value without listing it in the dependency array means the effect keeps using the value from whichever render it was last scheduled in, not the current one (a stale closure).
2. **Inline object/function literal dependencies** — a new reference every render means `Object.is` never reports "unchanged," so the effect re-runs on every single render regardless of whether anything meaningful changed.
3. **Forgetting cleanup** — every subscription, timer, or connection set up in an effect needs a matching teardown in its returned cleanup function, or repeated runs leak more of them each time.
4. **Fetching without a stale-request guard** — without an `ignore` flag or `AbortController`, a slower earlier request can resolve after a faster later one and overwrite correct state with stale data.
5. **Using `useEffect` to compute derived data** — if a value can be computed directly from existing props/state during render, computing it in an effect and storing it with `useState` adds an unnecessary extra render and a window where the UI shows stale derived data; just compute it inline (or with `useMemo` if the computation is expensive).
6. **Using `useEffect` where an event handler belongs** — code that should run in *response to a user action* (e.g. "when the user submits, POST the form") belongs in the event handler that fires on submit, not in an effect watching for state changes caused by that handler; using an effect adds an indirect, harder-to-trace path for something that has a direct one.

## Quick Reference

```
useEffect(() => { ... })                  → runs after every render
useEffect(() => { ... }, [])              → runs once, after mount only
useEffect(() => { ... }, [a, b])          → runs after mount, and when a or b changes (Object.is)
useEffect(() => { return () => {...} })   → cleanup: runs before next run AND on unmount
let ignore = false; ... return () => { ignore = true }  → stale-fetch guard
new AbortController(); ... return () => controller.abort()  → cancels an in-flight request
```
