05 — The Component Lifecycle — Mount, Update, Unmount, and Why Hooks Replaced It
A wall of componentDidMount-style methods was my first meeting with the component lifecycle, and I had to relearn it as something simpler. The model that made it click: every component moves through three phases — mount, update, unmount — and modern functional code expresses all three through the useEffect hook, not through a dozen class methods. [1][2] The class lifecycle is legacy knowledge; the lifecycle itself is not.
The three phases
Regardless of class vs function, a component's life has three phases [1][2]:
- Mounting — the component is added to the DOM for the first time. Initial render runs, then any setup effects.
- Updating — state or props change, the component re-renders, and effects may re-run depending on their dependencies.
- Unmounting — the component is removed from the DOM. Any cleanup from prior effects runs here.
That's the entire lifecycle. The question is only how I tap into each phase.
The class-era methods (and why they're mostly gone)
Class components exposed a method for each moment in the cycle — componentDidMount for initial setup, componentDidUpdate for responding to changes, componentWillUnmount for teardown, plus the now-deprecated componentWillMount/componentWillReceiveProps/componentWillUpdate trio [2]. I list them because I still read them in older code, but the official guidance is clear: don't write new class components, and don't reach for these methods. They split related logic across multiple methods (the setup lives in DidMount, the cleanup in WillUnmount, the dependency check in DidUpdate), which is exactly the fragmentation hooks were designed to fix.
useEffect: one hook for all three phases
useEffect is the functional-era replacement for the lifecycle methods, and its genius is that it unifies setup, update, and cleanup into a single construct [1][3]. An effect is a function that:
- runs after mount (replacing componentDidMount),
- runs again whenever its dependency array changes (replacing componentDidUpdate),
- optionally returns a cleanup function that runs before the next run and on unmount (replacing componentWillUnmount).
useEffect(() => {
const id = setInterval(tick, 1000); // setup — mount + dep changes
return () => clearInterval(id); // cleanup — before next run + unmount
}, []); // deps: run once on mountThat one block covers all three phases. The dependency array is the lever: [] means "run on mount only," [a, b] means "run on mount and whenever a or b changes," and omitting the array entirely means "run after every render" (almost never what I want).
The effect lifecycle, in detail
The official docs frame effects in terms of a reactive effect lifecycle — synchronize, then resynchronize, then clean up [1]. Each effect is its own lifecycle, independent of the component's other effects:
- On mount, React runs the effect (setup).
- If a dependency changes, React first runs the previous effect's cleanup, then runs the new effect (resynchronize).
- On unmount, React runs the final cleanup.
The practical consequence: every side effect I create must have a matching teardown. A subscription needs an unsubscribe. A timer needs a clearInterval. An event listener needs a removeEventListener. Forgetting cleanup is the single most common useEffect bug — it leaks listeners, fires timers on unmounted components, and double-fires in Strict Mode (which intentionally runs effects twice to surface exactly this class of bug).
When effects are NOT the answer
The lifecycle framing can make useEffect feel like the answer to everything. It isn't. Effects are for synchronizing with external systems — fetching, subscribing, talking to browser APIs, integrating with non-React libraries [1]. They are not for:
- Transforming data — if a value can be computed from existing state/props during render, compute it during render, not in an effect.
- Responding to user events — event handlers run on the event; effects run after render. Putting event logic in an effect creates stale-state bugs.
- Resetting state on prop change — the official guidance is to render a fresh component (via key) rather than sync state in an effect.
The rule of thumb I use: if I'm writing an effect and the body doesn't touch anything outside React, I've probably made a mistake. Effects exist to reach across the boundary to the outside world.
How I use this
I never write class components, so the lifecycle methods are read-only knowledge for me. For new code I reach for useEffect with three habits baked in: always pair setup with cleanup, always pass an explicit dependency array (never omit it), and ask "is this really synchronizing with an external system?" before I write the effect. When I find myself using an effect to derive state, I refactor to compute during render; when I find myself using one to react to a user action, I move the logic into the event handler. The lifecycle is still real, but I express it through one hook and only when I genuinely need to cross out of React.
References
[1] React team, "Lifecycle of reactive effects," react.dev, 2024. [Online]. Available: https://react.dev/learn/lifecycle-of-reactive-effects
[2] React team, "Class Component (legacy reference)," react.dev, 2024. [Online]. Available: https://react.dev/reference/react/Component
[3] React team, "You might not need an Effect," react.dev, 2024. [Online]. Available: https://react.dev/learn/you-might-not-need-an-effect
[4] W. Wojtekmaj, "React lifecycle methods diagram," projects.wojtekmaj.pl, 2023. [Online]. Available: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
[5] R. Wieruch, "React component lifecycle: methods and hooks," tsh.io blog, 2023. [Online]. Available: https://tsh.io/blog/react-component-lifecycle-methods-vs-hooks/
Knowledge check · Question 1 of 5
A component's lifecycle has three phases:
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!