Class lifecycle methods vs useEffect
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.
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 method | When |
|---|---|
componentDidMount | After first render |
componentDidUpdate(prevProps, prevState) | After every subsequent render |
componentWillUnmount | Before removal |
A single concern (e.g., subscribe to user.id) lives in three places:
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:
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
| Class | Hooks equivalent |
|---|---|
componentDidMount | useEffect(fn, []) |
componentDidUpdate | useEffect(fn, [deps]) |
componentWillUnmount | cleanup returned from useEffect |
getDerivedStateFromProps | usually compute during render or useMemo |
shouldComponentUpdate | React.memo |
getSnapshotBeforeUpdate | useLayoutEffect |
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, nothis.propssnapshots leaking across renders. - Effects respond to deps, not lifecycle moments — bugs from forgotten
componentDidUpdatebranches 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.