# Component Lifecycle

## 1. Three Phases, Not a List of Methods

Every component instance goes through the same three phases, regardless of whether it's written as a class or a function:

1. **Mount** — the component is created and its output is inserted into the DOM for the first time.
2. **Update** — the component re-renders because its props, state, or a context it reads changed (see "Rendering and reconciliation" for exactly what triggers this).
3. **Unmount** — the component is removed from the tree and its DOM node is torn down.

Class components exposed these phases as named methods you override. Function components don't have named lifecycle methods at all — `useEffect` (and friends) let you attach behavior to phases by describing *when* an effect should re-run, not by naming the phase directly. Both are two different APIs over the exact same underlying phases, produced by the exact same commit-phase mechanics (see section 6).

## 2. The Class Component Lifecycle

```tsx
class UserProfile extends React.Component<{ userId: string }, { user: User | null }> {
  constructor(props: { userId: string }) {
    super(props);
    this.state = { user: null }; // set up initial state — runs once, before mount
  }

  componentDidMount() {
    // runs once, right after the component's DOM node is inserted
    fetchUser(this.props.userId).then((user) => this.setState({ user }));
  }

  componentDidUpdate(prevProps: { userId: string }) {
    // runs after every re-render EXCEPT the first — always compare props/state
    // yourself, since this fires on every update, not just the relevant one
    if (prevProps.userId !== this.props.userId) {
      fetchUser(this.props.userId).then((user) => this.setState({ user }));
    }
  }

  componentWillUnmount() {
    // runs once, right before the DOM node is removed — cancel subscriptions,
    // timers, in-flight requests here
    this.pendingRequest?.cancel();
  }

  render() {
    return this.state.user ? <p>{this.state.user.name}</p> : <p>Loading…</p>;
  }
}
```

Note the shape all three share: `render` is called to compute *what* to show, and `componentDidMount`/`componentDidUpdate`/`componentWillUnmount` are called around it to run side effects tied to *when* it happened. This is the split hooks eventually generalized.

## 3. The Function-Component Equivalent: `useEffect`

```tsx
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    let cancelled = false;
    fetchUser(userId).then((u) => {
      if (!cancelled) setUser(u);
    });
    return () => {
      cancelled = true; // cleanup — this is componentWillUnmount's job, reused
    };
  }, [userId]); // re-run only when userId actually changes

  return user ? <p>{user.name}</p> : <p>Loading…</p>;
}
```

This one `useEffect` call replaces `componentDidMount` **and** `componentDidUpdate` **and** `componentWillUnmount` at once — see the mapping table in section 4. The full mechanics of the dependency array, cleanup timing, and common bugs are covered in depth in the "UseEffect" topic; this topic focuses on how that mapping lines up with the class model and the commit phase underneath both.

![Class lifecycle methods and their useEffect equivalents, lined up against the mount/update/unmount phases and the render/commit split each one fires in](diagrams/01-lifecycle-mapping.svg)

## 4. The Mapping, Precisely

| Class method | Fires | `useEffect` equivalent |
|---|---|---|
| `constructor` | Once, before first render | `useState(initialValue)` (lazy initializer form for expensive setup) |
| `render` | Every render | The function component's own body |
| `componentDidMount` | Once, after first commit | `useEffect(() => { ... }, [])` |
| `componentDidUpdate` | After every commit except the first | `useEffect(() => { ... }, [deps])` — runs after mount **and** whenever a listed dependency changes; you don't get a "not the first time" flag for free, you compare inside the effect if you truly need mount excluded |
| `componentWillUnmount` | Once, before removal | The function **returned** from `useEffect`'s callback |
| `getDerivedStateFromProps` | Before every render, mount and update | Computing a value directly in the render body from props (no effect needed) |
| `shouldComponentUpdate` | Before re-rendering, to allow bailing out | `React.memo` (covered in "Performance optimization") |
| `getSnapshotBeforeUpdate` | Right before the DOM is mutated, after render | `useLayoutEffect` reading the DOM just before your own update (covered in "UseLayoutEffect") |

The important asymmetry: a single `useEffect` with a dependency array covers mount, every relevant update, *and* unmount cleanup — three class methods collapse into one hook because the hook is keyed on "did this value change," not on "which named phase are we in."

## 5. Cleanup Runs Before the *Next* Effect, Not Just on Unmount

This is the detail that trips people coming from classes the hardest: the cleanup function isn't only for unmount. React runs it before *every* re-run of the effect, including ordinary updates — not just the final unmount.

```tsx
useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);
  return () => clearInterval(id); // runs before the NEXT effect AND on unmount
}, [intervalMs]);
```

If `intervalMs` changes, React runs the cleanup from the *previous* effect (clearing the old interval) before running the *new* effect (starting a new one with the updated delay) — in that order, every time. On final unmount, there is no "next effect" to run, so only the cleanup fires. This single rule is why `useEffect` can replace `componentDidMount` + `componentDidUpdate` + `componentWillUnmount` with one function: "clean up the old thing, then set up the new thing" describes an update, and "clean up the old thing, don't set anything up" describes an unmount — same cleanup call, different question of whether a following effect exists.

## 6. Under the Hood: What Actually Runs, and When

### Fibers carry a `tag` that tells the commit phase which lifecycle model to run

Every fiber has a numeric `tag` (`ClassComponent`, `FunctionComponent`, `HostComponent`, etc.). During the commit phase, React switches on this tag to decide what side-effect code to invoke for that fiber — `commitClassLifeCycles` for class fibers (which directly calls `instance.componentDidMount()` / `instance.componentDidUpdate(prevProps, prevState)` on the class instance sitting at `fiber.stateNode`), versus effect-list processing for function fibers. Class lifecycle methods are not some separate mechanism layered on top of fibers — they are function calls React makes directly against your class instance, from inside the same commit walk that also updates the DOM.

### Function-component effects live on `fiber.updateQueue`, not `memoizedState` alone

Each `useEffect` call appends a hook node to the fiber's hooks linked list (`memoizedState`, described in the "State" topic's Under the Hood section) *and* an effect object — `{ tag, create, destroy, deps }` — onto a separate circular list at `fiber.updateQueue`. `tag` records whether this is a passive effect (`useEffect`) or a layout effect (`useLayoutEffect`); `create` is your callback; `destroy` is whatever your callback returned last time (or `undefined`); `deps` is the array React compares next render.

### Dependency comparison happens before the effect list is even scheduled

On each render, React compares the new `deps` array against the previous one **element-by-element with `Object.is`** (not the array reference itself — a fresh `[userId]` array every render is fine, it's `userId` that must be stable). If every element matches, React tags this effect as "no work needed" and its `create`/`destroy` are skipped entirely for this commit — the hook node's `deps` are still updated, but nothing runs. This is exactly the same `Object.is` comparator used for `React.memo`'s props check (see "Rendering and reconciliation"); it's one general mechanism reused in multiple hooks.

### Commit order: mutations → layout effects (sync) → paint → passive effects (async)

The commit phase is not "run everything for this fiber, then move to the next fiber." It's three sweeps across the *whole* tree of committed fibers, in this order:

1. **Mutation sweep** — DOM insertions/updates/removals happen. For unmounting fibers, this is also where `componentWillUnmount` and any `useLayoutEffect` cleanup fire — bottom-up, children before their parents, because a parent's DOM node might still be needed as a reference point for removing its children first. Both run **synchronously**, before anything paints.
2. **Layout sweep** — remaining `useLayoutEffect` callbacks and `componentDidMount`/`componentDidUpdate` run here, synchronously, before the browser paints. This is why layout effects can read/write layout without a visible flash — nothing has been painted yet.
3. **Passive sweep** — `useEffect` callbacks run here, asynchronously, scheduled *after* the browser has painted. A `useEffect` cleanup for an **unmounting** component is deferred into this same asynchronous sweep too, not the synchronous mutation sweep — which is why a `useEffect` cleanup can observably fire later than a sibling class's `componentWillUnmount` for the same removal, even though both are "unmount cleanup."

```
commit:
  mutation sweep   → DOM changes; componentWillUnmount + useLayoutEffect cleanup (bottom-up, sync)
  layout sweep     → useLayoutEffect setup, componentDidMount/Update            (sync, pre-paint)
  ── browser paints ──
  passive sweep    → useEffect setup; useEffect cleanup for unmounted trees too  (async, post-paint)
```

### Why unmount cleanup order is bottom-up

Because the mutation sweep must remove a parent's DOM node only after its children have already been detached (or the child's own cleanup — which might try to read `parentNode`, or unsubscribe from something the parent set up — would run against a half-torn-down tree), React always unmounts a subtree depth-first, calling every descendant's cleanup/`componentWillUnmount` before it calls the parent's. This isn't a documented "nice to know" — it's a direct consequence of doing a single DOM removal pass without leaving dangling references mid-walk.

## 7. Common Pitfalls

1. **Expecting `componentDidUpdate` (or a bare `useEffect(fn)` with no dependency array) to run only when "the relevant thing" changed** — `componentDidUpdate` fires after *every* update regardless of cause, and an effect with no dependency array runs after *every single render*. Both require you to compare old vs. new yourself, or supply a dependency array, if you only care about specific changes.
2. **Putting side effects directly in the render/component body** instead of `componentDidMount`/`useEffect`** — the render phase (or a function component's body) can run more than once per commit or be thrown away entirely (see "Rendering and reconciliation"); only commit-phase hooks are guaranteed to run exactly once per actual DOM update.
3. **Forgetting `componentWillUnmount`/effect cleanup entirely** — subscriptions, intervals, and event listeners set up in `componentDidMount`/`useEffect` keep running (and keep references alive) after the component is gone, unless you tear them down in the matching cleanup.
4. **Missing a dependency in `useEffect`'s array** — the effect keeps closing over a stale value from whatever render created it (this is the JavaScript **Closures** topic's "closures capture variables, not values" playing out inside a hook); the effect doesn't magically see updated outer variables just because they changed elsewhere.
5. **Assuming cleanup only runs on unmount** — as section 5 explains, it runs before every subsequent effect re-run too; code that assumes "this only happens once, at the very end" will run far more often than expected for a changing dependency.
6. **Calling `setState` unconditionally inside `componentDidUpdate`** without a guard — since `componentDidUpdate` fires after every update including the one this `setState` itself causes, an unguarded call creates an infinite re-render loop. The functional equivalent, an unconditional `setState` inside `useEffect` with no dependency array, has the identical failure mode.

## 8. Quick Reference

| Phase | Class method(s) | Hook equivalent | Runs during |
|---|---|---|---|
| Mount | `constructor`, `render`, `componentDidMount` | function body runs, then `useEffect(fn, [])` | render phase, then layout/passive sweep |
| Update | `render`, `componentDidUpdate` | function body runs, then `useEffect(fn, [deps])` if deps changed | render phase, then layout/passive sweep |
| Unmount | `componentWillUnmount` | cleanup function returned from `useEffect` | mutation sweep, bottom-up |
| Bail out of updating | `shouldComponentUpdate` | `React.memo` | before the render phase even calls the function |
| Read DOM right before it changes | `getSnapshotBeforeUpdate` | `useLayoutEffect` | layout sweep, pre-paint |

```
useEffect(() => {
  setup();
  return () => cleanup(); // runs before next effect, AND on unmount
}, [deps]);

deps omitted   -> runs after every render (mount + every update)
deps = []      -> runs once, after mount only
deps = [a, b]  -> runs after mount, and after any render where a or b changed
                  (Object.is comparison, element by element)
```
