Back to React
React
easy
mid

How do class lifecycle methods map to useEffect in React?

Class lifecycles split a single concern across multiple methods (`componentDidMount` + `componentDidUpdate` + `componentWillUnmount`). `useEffect` lets you co-locate the whole effect — setup + cleanup — and run it whenever its dependencies change. Effects are about synchronization, not lifecycle moments.

4 min read·~10 min to think through

Class lifecycle methods and useEffect aim at the same problem — running side effects — but they think about it differently. The hooks shift is conceptual, not just syntactic.

Class lifecycle — by moment

Class methodWhen
componentDidMountAfter first render
componentDidUpdate(prevProps, prevState)After every subsequent render
componentWillUnmountBefore removal

A single concern (e.g., subscribe to user.id) lives in three places:

jsx
componentDidMount() { subscribe(this.props.userId); }
componentDidUpdate(prev) {
  if (prev.props.userId !== this.props.userId) {
    unsubscribe(prev.props.userId);
    subscribe(this.props.userId);
  }
}
componentWillUnmount() { unsubscribe(this.props.userId); }

useEffect — by synchronization

You describe the effect and its cleanup in one place; React runs it whenever the dependencies change, with cleanup before each re-run and before unmount:

jsx
useEffect(() => {
  subscribe(userId);
  return () => unsubscribe(userId);
}, [userId]);

This co-locates what to set up and how to tear it down. The mental model shifts from "do X at moment Y" to "keep this side effect synchronized with these inputs."

Mapping the lifecycles

ClassHooks equivalent
componentDidMountuseEffect(fn, [])
componentDidUpdateuseEffect(fn, [deps])
componentWillUnmountcleanup returned from useEffect
getDerivedStateFromPropsusually compute during render or useMemo
shouldComponentUpdateReact.memo
getSnapshotBeforeUpdateuseLayoutEffect

Why hooks won

  • Co-location — one effect, one place, including cleanup.
  • Composition — wrap an effect into a custom hook (useSubscribe(userId)) and reuse.
  • No this — no method binding, no this.props snapshots leaking across renders.
  • Effects respond to deps, not lifecycle moments — bugs from forgotten componentDidUpdate branches go away when the deps array is honest.

The trap

The biggest hook gotcha is stale closures — effects capture values from the render they ran in. The deps array is how you tell React "re-sync when these change." Lint enforces it.

Interview framing

"Class lifecycles split a single concern — subscribe to userId — across three methods: didMount, didUpdate with a prev-prop guard, and willUnmount. useEffect lets you write it once: setup, return cleanup, list the deps. The mental shift is from 'do X at moment Y' to 'keep this effect synchronized with these inputs.' The cost is the closure model — effects see the values from their render, so the deps array has to be complete or you get staleness."

Follow-up questions

  • How does cleanup work between renders in useEffect?
  • What's the equivalent of getSnapshotBeforeUpdate in hooks?
  • Why are stale closures a class of bug that didn't exist in class components?

Common mistakes

  • Translating mount/update/unmount 1:1 instead of thinking in synchronization.
  • Omitting dependencies → stale values.
  • Putting every effect in one big useEffect instead of one per concern.

Performance considerations

  • Effects run after paint by default (good for perceived perf). `useLayoutEffect` runs synchronously before paint — use sparingly. Stable references via `useCallback`/`useMemo` keep deps arrays from churning.

Edge cases

  • Race conditions in data fetching — cleanup must cancel.
  • useLayoutEffect for measurements before paint.
  • Empty deps array `[]` capturing stale closures.

Real-world examples

  • Subscriptions, timers, event listeners, data fetching.
  • Migrating a class component to hooks: each lifecycle becomes a focused `useEffect` with the right deps.

Senior engineer discussion

Seniors think in synchronization, not lifecycle moments — one effect per concern, full deps, cleanup that's symmetric to setup, and custom hooks for reuse. They use `useLayoutEffect` deliberately for measurement, not as a default.

Related questions