Which Class Lifecycle Methods Does useEffect Replace?
Advertisement
🧩 Scenario
Architecture Walkthrough
The Core Three: Mount, Update, Unmount
Class components have three primary lifecycle methods for side effects. componentDidMount runs once after the component is inserted into the DOM. componentDidUpdate runs after every re-render when props or state change. componentWillUnmount runs once before the component is removed. All three map directly to useEffect patterns.
useEffect(() => fn, []) (empty array) runs fn once after the initial render, equivalent to componentDidMount. useEffect(() => fn, [dep]) runs fn after every render where dep changed, equivalent to componentDidUpdate scoped to that dependency. Returning a function from either replaces componentWillUnmount: the returned function is the cleanup, called when the component unmounts or before the effect runs again on the next render.
The key behavioral difference is that useEffect runs both on mount and on updates in the dep-array form. Class componentDidUpdate had an if (prevProps.userId !== this.props.userId) guard to skip the first run. The dep array handles this automatically: the effect does not run if the values in the array have not changed since the last render.
getDerivedStateFromProps: Derive During Render
getDerivedStateFromProps was a class lifecycle that returned derived state based on incoming props. The hook equivalent is not useEffect — computing derived state in an effect causes an extra render cycle (effect runs, sets state, triggers a second render). The correct pattern is to compute derived values directly during the render function body, or with useMemo if the computation is expensive. The rule is simple: if a value can be computed from props or state, compute it during render, not in an effect.
shouldComponentUpdate and React.memo
shouldComponentUpdate allowed class components to skip re-renders when props had not changed. The function component equivalent is React.memo, which wraps a component and performs a shallow comparison of props before deciding whether to re-render. A custom comparison function can be passed as the second argument for deep comparisons.
useLayoutEffect for Pre-Paint Work
getSnapshotBeforeUpdate ran just before the DOM was updated, allowing the component to capture scroll position or measurements before they changed. useLayoutEffect is the closest hook equivalent: it runs synchronously after the DOM is updated but before the browser paints. Use it for DOM reads that must happen before the user sees the update, such as measuring an element to position something else. For most effects, useEffect (runs after paint, asynchronous) is correct.
Key Code Explained
import { useEffect, useLayoutEffect, useState, useCallback, useRef } from 'react';
// componentDidMount equivalent: fetch data once on mount
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Empty dep array = runs once after mount, like componentDidMount
const controller = new AbortController();
async function fetchUser() {
const data = await fetchUserById(userId, { signal: controller.signal });
setUser(data);
}
fetchUser();
// cleanup = componentWillUnmount (also runs before re-running the effect)
return () => controller.abort();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Note: userId is omitted here intentionally — this mirrors componentDidMount
// If you need to re-fetch on userId change, add it to the deps array
return user ? <div>{user.name}</div> : <Skeleton />;
}
// componentDidUpdate on a specific prop equivalent
function OrderHistory({ userId }: { userId: string }) {
const [orders, setOrders] = useState<Order[]>([]);
useEffect(() => {
// Runs on mount AND whenever userId changes — equivalent to:
// componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) { ... } }
fetchOrdersByUser(userId).then(setOrders);
}, [userId]); // re-run when userId changes
return <OrderList orders={orders} />;
}
// componentWillUnmount cleanup: event listener
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll, { passive: true });
// returned function = componentWillUnmount
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <p>Scroll position: {scrollY}px</p>;
}
// getDerivedStateFromProps equivalent: derive during render, no useEffect
function FilteredList({ items, searchQuery }: { items: Item[]; searchQuery: string }) {
// Correct: derive filtered list during render — no state, no useEffect
const filteredItems = items.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
// If items is large and filtering is expensive, useMemo is the right tool:
// const filteredItems = useMemo(
// () => items.filter(...),
// [items, searchQuery]
// );
return <ul>{filteredItems.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}
// getSnapshotBeforeUpdate equivalent: useLayoutEffect
function Chat({ messages }: { messages: Message[] }) {
const listRef = useRef<HTMLUListElement>(null);
useLayoutEffect(() => {
// Runs after DOM update, before browser paints
// Safe to read/write layout here without causing a visible flicker
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}, [messages]); // runs after every messages update
return (
<ul ref={listRef} className="chat-list">
{messages.map((m) => <li key={m.id}>{m.text}</li>)}
</ul>
);
}
The useLayoutEffect for auto-scrolling chat is the canonical example. If you used useEffect instead (which runs after paint), the user would briefly see the unscrolled position before the scroll adjustment, causing a visible flicker. useLayoutEffect fires synchronously after the DOM mutation but before the browser draws the frame, so the scroll is in place before the user sees anything.
Tradeoffs
| Class lifecycle | Hook equivalent | Key difference |
|---|---|---|
| componentDidMount | useEffect(fn, []) | Hook runs after every unmount/remount cycle, not just initial mount |
| componentDidUpdate(prevProps, prevState) | useEffect(fn, [dep]) | Hook scopes to specific deps rather than getting all prev values |
| componentWillUnmount | Cleanup return from useEffect | Same timing |
| getDerivedStateFromProps | Derived value during render | Avoids extra render caused by setting state in useEffect |
| shouldComponentUpdate | React.memo with optional comparator | memo is shallower by default |
| getSnapshotBeforeUpdate | useLayoutEffect | Different API but same pre-paint timing |
What Interviewers Actually Check
- Whether you know all three useEffect dep-array patterns and what lifecycle each corresponds to
- Whether you know
getDerivedStateFromPropsshould be replaced by deriving during render, not useEffect - Whether you know
React.memoas the equivalent ofshouldComponentUpdate - Whether you know the difference between
useEffect(after paint) anduseLayoutEffect(before paint) - Whether you know
componentWillMountis deprecated and why fetching there was wrong
Follow-Up Questions
- Why was
componentWillMountdeprecated? What problems did fetching data there cause in React 18 Strict Mode? - How does
useInsertionEffect(React 18) differ fromuseLayoutEffect? When does it fire? - A class component uses
PureComponentfor optimization. What is the function component equivalent and what are its limitations? - How do React 18's concurrent features affect the timing guarantees that lifecycle methods provided in class components?
- In a class component,
componentDidUpdatereceivesprevPropsandprevState. How do you access the previous value of a prop in a function component?
Common Candidate Mistakes
- Using
useEffectwith no dependency array instead of an empty array[]— runs after every render, not once - Putting derived state logic in
useEffectto mimicgetDerivedStateFromProps— causes a double render - Returning a non-function value from
useEffect— React ignores non-function returns silently - Using
useLayoutEffecteverywhere instead ofuseEffect— blocks the browser from painting, causing performance issues - Not realizing that the cleanup function from
useEffectalso runs before the effect re-runs (not only on unmount)
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you map componentDidMount, componentDidUpdate, and componentWillUnmount to their useEffect patterns?
- Can you explain why
getDerivedStateFromPropsshould be replaced by deriving during render? - Can you name the hook equivalent of
shouldComponentUpdate? - Can you explain the difference between
useEffectanduseLayoutEffectin terms of when each fires relative to browser paint? - Can you read the previous value of a prop in a function component?
Summary
Three class lifecycle methods map directly to useEffect patterns. componentDidMount becomes useEffect(fn, []) with an empty dependency array. componentDidUpdate scoped to specific props or state becomes useEffect(fn, [dep]) with those values in the array. componentWillUnmount becomes the function returned from useEffect. getDerivedStateFromProps has no hook equivalent and should be replaced by computing derived values directly during the render function, or with useMemo for expensive computations. shouldComponentUpdate is replaced by React.memo with an optional comparator. getSnapshotBeforeUpdate (pre-paint DOM reads) is replaced by useLayoutEffect, which runs synchronously after DOM mutation but before the browser paints. The key design difference is that hooks are declarative and dep-array based, while class lifecycles were imperative with explicit prev/current comparisons.
Can one useEffect replace all three lifecycle methods at once?
No. One useEffect covers one timing and one dependency scope. componentDidMount is an empty-dep useEffect. componentDidUpdate on a specific value is a useEffect with that value in the deps array. componentWillUnmount is the cleanup return inside an empty-dep useEffect. You typically need separate useEffect calls for each distinct concern.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement