Which Class Lifecycle Methods Does useEffect Replace?

Intermediate10 min interview
Skills tested:
Mapping componentDidMount, componentDidUpdate, and componentWillUnmount to useEffect patternsKnowing that getDerivedStateFromProps is replaced by deriving values during render, not in useEffectKnowing that shouldComponentUpdate is replaced by React.memoUnderstanding useLayoutEffect as the equivalent of getSnapshotBeforeUpdate for pre-paint DOM readsRecognizing that class lifecycle methods run in a fixed order while hooks are declarative

Advertisement

🧩 Scenario

This is a standard React interview question for candidates who may have worked with class components. Interviewers look for the complete mapping and the nuances: what has no direct hook equivalent (and how to handle it), and the useLayoutEffect distinction.

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 lifecycleHook equivalentKey difference
componentDidMountuseEffect(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
componentWillUnmountCleanup return from useEffectSame timing
getDerivedStateFromPropsDerived value during renderAvoids extra render caused by setting state in useEffect
shouldComponentUpdateReact.memo with optional comparatormemo is shallower by default
getSnapshotBeforeUpdateuseLayoutEffectDifferent 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 getDerivedStateFromProps should be replaced by deriving during render, not useEffect
  • Whether you know React.memo as the equivalent of shouldComponentUpdate
  • Whether you know the difference between useEffect (after paint) and useLayoutEffect (before paint)
  • Whether you know componentWillMount is deprecated and why fetching there was wrong

Follow-Up Questions

  1. Why was componentWillMount deprecated? What problems did fetching data there cause in React 18 Strict Mode?
  2. How does useInsertionEffect (React 18) differ from useLayoutEffect? When does it fire?
  3. A class component uses PureComponent for optimization. What is the function component equivalent and what are its limitations?
  4. How do React 18's concurrent features affect the timing guarantees that lifecycle methods provided in class components?
  5. In a class component, componentDidUpdate receives prevProps and prevState. How do you access the previous value of a prop in a function component?

Common Candidate Mistakes

  • Using useEffect with no dependency array instead of an empty array [] — runs after every render, not once
  • Putting derived state logic in useEffect to mimic getDerivedStateFromProps — causes a double render
  • Returning a non-function value from useEffect — React ignores non-function returns silently
  • Using useLayoutEffect everywhere instead of useEffect — blocks the browser from painting, causing performance issues
  • Not realizing that the cleanup function from useEffect also 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 getDerivedStateFromProps should be replaced by deriving during render?
  • Can you name the hook equivalent of shouldComponentUpdate?
  • Can you explain the difference between useEffect and useLayoutEffect in 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.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

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