In React, how do the class lifecycle methods componentDidMount, componentDidUpdate and componentWillUnmount map onto useEffect, and where does that mapping mislead you?
answer
- one effect, not three callbacks
- deps compare, they do not sequence
- cleanup runs before every re-run
- no prevProps argument exists
- effects run after paint, unlike componentDidMount
basics
~20 suseEffect with empty dependencies approximates componentDidMount, its returned cleanup approximates componentWillUnmount, and non-empty dependencies approximate componentDidUpdate. The mapping misleads because one effect covers all three, and cleanup runs before every re-run, not only at unmount.
solid answer
~40 sThe rough table is: `useEffect(fn, [])` for componentDidMount, the function returned from the effect for componentWillUnmount, and `useEffect(fn, [dep])` for componentDidUpdate. But it is one effect, not three callbacks: React runs it after the first commit and re-runs it whenever a dependency changes by `Object.is`, always running the previous cleanup first. So cleanup is not "unmount code" — it is "undo the last run". There is no `prevProps` argument, so the manual `if (prevProps.id !== this.props.id)` guard from componentDidUpdate becomes the dependency array, and there is no built-in way to skip the first render. The honest framing is that an effect describes how to keep the component synchronized with an external system for the current props and state, which is why it is not componentDidMount plus componentDidUpdate glued together.
code
jsx · 12 linesimport { useEffect, useState } from 'react';
export function Ticker({ intervalMs }) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), intervalMs);
return () => clearInterval(id);
}, [intervalMs]);
return <p>{count}</p>;
}go deeper
Be able to state the three-way mapping out loud and to write the small version: setup in the effect body, teardown in the returned function, the values it depends on in the array.
Explain that React re-runs the effect when a dependency changes by Object.is and runs the previous cleanup first, so cleanup means 'undo the last run' rather than 'the component is going away'.
Show judgment in a migration: spot the ported componentDidUpdate that now fires on mount, the teardown that assumed it ran once, and the measurement that flickers because useEffect runs after paint rather than before it.
Own the framing your team codes by. If people keep porting lifecycles literally, the fix is not more review comments but a shared rule that effects describe synchronization with external systems, and that anything triggered by a user action belongs in a handler.
## The mapping candidates memorize Every migration guide prints roughly the same table: | Class method | Hook shape | | --- | --- | | componentDidMount | `useEffect(() => { ... }, [])` | | componentDidUpdate | `useEffect(() => { ... }, [dep])` | | componentWillUnmount | the function returned from the effect | It is close enough to pass a screening question, and it is exactly why so much migrated code is subtly wrong. ## The same component both ways A class that keeps a connection in sync with a `roomId` prop needs three methods, and the update path duplicates the other two: ```jsx class Chat extends React.Component { componentDidMount() { this.conn = connect(this.props.roomId); } componentDidUpdate(prevProps) { if (prevProps.roomId !== this.props.roomId) { this.conn.close(); this.conn = connect(this.props.roomId); } } componentWillUnmount() { this.conn.close(); } render() { return <h1>{this.props.roomId}</h1>; } } ``` The function version says the same thing once: ```jsx function Chat({ roomId }) { useEffect(() => { const conn = connect(roomId); return () => conn.close(); }, [roomId]); return <h1>{roomId}</h1>; } ``` The class version is a sequence of moments — mounted, updated, about to be destroyed. The hook version is a rule — for this `roomId`, this connection should exist; when the value changes, tear the old one down and set the new one up. Nothing in the hook version mentions mounting. ## Where the mapping breaks **Cleanup is not unmount code.** React runs the cleanup returned by the previous run before every re-run, and once more when the component is removed. Code written as "componentWillUnmount, therefore this only happens once, at the end" leaks or double-registers as soon as a dependency changes. In the class, that teardown-then-setup pairing had to be hand-written inside componentDidUpdate, and forgetting it was the classic class-era subscription leak. **There is no prevProps.** The class compared previous and current values itself. In a function component the dependency array is that comparison: React stores the previous array and compares each entry with `Object.is`. You do not get the old value handed to you, and reintroducing it with a ref that stores the last props is a sign you are still thinking in lifecycles. **There is no "skip the first render".** componentDidUpdate ran only on updates. An effect always runs after the first commit, so a literal port of update-only logic fires once too often. People bolt on a `useRef(true)` first-render flag; usually the real fix is that the code was never an effect at all — it was a response to a user interaction and belongs in the event handler. **The timing is different.** componentDidMount and componentDidUpdate run synchronously after React commits to the DOM and before the browser paints. `useEffect` is deferred until after paint. The hook whose timing actually matches the class methods is `useLayoutEffect`. For most work the difference is invisible; for reading layout and adjusting it, the difference is a visible flicker. **Instance fields versus closures.** `this.conn` is one slot that survives every render. A function body re-runs from scratch each render, and each effect closes over the props and state of the render that created it. That is a feature — each run sees a consistent snapshot — but it is a different model from mutating `this`, and code that assumed a single long-lived instance has to move that value into a ref deliberately. ## How to answer it Give the table, then immediately say why it is a teaching crutch: one effect replaces three methods because an effect is not a lifecycle callback but a synchronization rule, and the dependency array is the comparison the class did by hand. The two concrete consequences worth naming are that cleanup runs on every re-run rather than only at unmount, and that effects run after paint while componentDidMount ran before it.
- The class version guarded its work with if (prevProps.userId !== this.props.userId). What replaces that guard in the hook version?The dependency array. Listing `userId` in the deps makes React compare the previous and current value with `Object.is` and re-run the effect only when it actually changed. You write the values the effect reads, not a comparison — the comparison is React's job, and hand-rolling it inside the effect body usually means the deps are wrong.
- How would you make an effect skip the very first render, the way componentDidUpdate did, and why is wanting that a warning sign?Mechanically, a ref initialized to true that you flip on the first run. But the need signals that the code is not synchronization at all: something that must happen on update but not on mount is usually a reaction to a specific user action, which belongs in the event handler that caused it, or a value that should simply be computed during render.
- Does useEffect fire at exactly the moment componentDidMount used to?No. componentDidMount ran synchronously after the DOM commit and before the browser painted; `useEffect` is deferred until after paint so it cannot block visual updates. `useLayoutEffect` is the timing equivalent. If migrated code measures or repositions an element and now flickers, that timing difference is usually the cause.
saying these in an interview costs you the question
- useEffect with an empty array is just componentDidMount renamed
- Cleanup only runs when the component unmounts
- You need three separate effects to replace the three methods
- The dependency array is only a performance optimization
- useEffect fires at the same moment componentDidMount did