Episode 2 — React Frontend Architecture NextJS / 2.4 — React Lifecycle Methods
Interview Questions: React Lifecycle Methods
How to use this material
- Read each question and think for 60 seconds before reading the answer
- Practice explaining out loud — interviewers want to hear your thought process
- Focus on the "Why interviewers ask" section to understand what they're really testing
- Build a 2-minute and a 30-second version of each answer
- Review weekly until second nature
Beginner (Q1–Q6)
Q1. What is the component lifecycle in React?
Why interviewers ask: They want to know if you understand the fundamental concept that components go through phases — mount, update, unmount — and that React provides hooks (lifecycle methods or useEffect) to run code at specific points in that lifecycle.
Model answer: Every React component goes through three main phases. Mounting is when it's created and inserted into the DOM — this is where you set up state, fetch initial data, and subscribe to events. Updating happens whenever props change, state changes, or the parent re-renders — this is where you respond to new data. Unmounting is when the component is removed from the DOM — this is where you clean up timers, event listeners, and network requests to prevent memory leaks. In class components, these phases have explicit methods like componentDidMount, componentDidUpdate, and componentWillUnmount. In functional components, useEffect handles all three phases in a single API.
Q2. What is componentDidMount and when does it fire?
Why interviewers ask: It's the most commonly used lifecycle method. They want to know you understand the correct place for side effects.
Model answer: componentDidMount fires once, immediately after the component is first rendered and its DOM nodes are created. This makes it the ideal place for side effects: fetching data from APIs, setting up subscriptions or event listeners, initialising third-party libraries that need DOM access, and measuring DOM elements. You can safely call setState in componentDidMount — it triggers a second render, but the user never sees the intermediate state because React batches the paint. The functional component equivalent is useEffect(() => {...}, []) with an empty dependency array.
Q3. What is useEffect and how does it relate to lifecycle methods?
Why interviewers ask: They're testing whether you understand the hooks mental model or are just memorising the lifecycle-to-hooks mapping table.
Model answer: useEffect is a hook that lets functional components synchronise with external systems — APIs, the DOM, timers, subscriptions. The key insight is that useEffect is not a lifecycle method replacement — it's a different mental model. Instead of asking "when does this run?" (mount, update, unmount), you ask "what does this effect synchronise with?" The dependency array declares the data the effect depends on, and React automatically runs it when those values change and cleans it up before the next run or on unmount. That said, the mapping is: useEffect(fn, []) ≈ componentDidMount, useEffect(fn, [dep]) ≈ componentDidUpdate for that dep, and the cleanup function ≈ componentWillUnmount.
Q4. Why is cleanup important in useEffect?
Why interviewers ask: Memory leaks are a real production problem. They want to know you prevent them.
Model answer: The cleanup function prevents memory leaks and stale behaviour. Every setup needs a corresponding teardown: addEventListener → removeEventListener, setInterval → clearInterval, fetch → AbortController.abort(), WebSocket.connect → .close(), observer.observe → .disconnect(). Without cleanup, event listeners accumulate, timers continue firing on unmounted components (causing "setState on unmounted component" warnings), and network requests complete and try to update components that no longer exist. The cleanup runs in two situations: before the effect re-runs when dependencies change, and when the component unmounts.
Q5. What is an error boundary in React?
Why interviewers ask: Error handling is a sign of production readiness. They want to know you don't just handle the happy path.
Model answer: An error boundary is a class component that catches JavaScript errors in its child component tree during rendering, lifecycle methods, and constructors. It uses two methods: static getDerivedStateFromError(error) to update state and show a fallback UI, and componentDidCatch(error, info) to log the error. Error boundaries do NOT catch errors in event handlers, async code, or server-side rendering. As of React 19, there's no hook equivalent — error boundaries must be class components. In practice, you wrap feature sections with error boundaries so a crash in the sidebar doesn't take down the whole app.
Q6. What is the difference between useEffect and useLayoutEffect?
Why interviewers ask: They want to see if you understand the rendering pipeline and when visual flicker is a concern.
Model answer: Both have identical APIs but differ in timing. useEffect runs after the browser paints the screen — it's non-blocking and used for 99% of effects. useLayoutEffect runs before the browser paints — it's synchronous and blocks painting. You use useLayoutEffect when your effect modifies the DOM in a way that the user would notice as a flicker — for example, measuring an element's position and then repositioning a tooltip. With useEffect, the user briefly sees the tooltip at position (0,0) before it jumps. With useLayoutEffect, the measurement and positioning happen before the user sees anything. Default to useEffect; only switch to useLayoutEffect if you observe visual flicker.
Intermediate (Q7–Q12)
Q7. Explain the stale closure problem in useEffect.
Why interviewers ask: This is the most common hooks bug. Debugging it requires deep understanding of JavaScript closures.
Model answer: Effects capture values from the render they were created in — they "close over" the current props and state. If an effect runs once (empty dependency array) and references a state variable, it captures the initial value forever. For example, a setInterval inside useEffect(() => {...}, []) that reads count will always see count as 0, even if the user clicks to increment it. The fix is either: (1) add the variable to the dependency array so the effect re-runs with the new value, (2) use the updater function form setCount(prev => prev + 1) which doesn't need to read the current value, or (3) store the latest value in a ref and read ref.current in the callback.
Q8. What are the deprecated lifecycle methods and why were they removed?
Why interviewers ask: Shows you understand React's evolution and the reasoning behind design decisions.
Model answer: Three methods were deprecated in React 16.3: componentWillMount, componentWillReceiveProps, and componentWillUpdate. They were removed because they ran during the "render phase" before the DOM was updated, which caused problems with React's new async rendering (Concurrent Mode). componentWillMount was misused for data fetching even though fetches can't complete before render. componentWillReceiveProps fired even when props hadn't actually changed, leading to bugs. componentWillUpdate was unreliable for reading DOM state because the DOM could change between the method call and the actual update. They were replaced by componentDidMount/constructor, getDerivedStateFromProps/componentDidUpdate, and getSnapshotBeforeUpdate respectively.
Q9. How do you prevent race conditions when fetching data in useEffect?
Why interviewers ask: Race conditions are a real-world data-integrity issue. This tests production awareness.
Model answer: Race conditions occur when multiple fetches fire in sequence (e.g., user types quickly in a search) and responses arrive out of order. The solution is AbortController. You create a controller, pass its signal to fetch, and in the cleanup function call controller.abort(). When the dependency changes, the cleanup cancels the previous request before the new effect starts a new one. This ensures only the latest request's response is used. An alternative is the ignore boolean flag pattern — set a variable let ignore = false, and in cleanup set ignore = true, then check if (!ignore) before calling setState. AbortController is better because it actually cancels the network request, saving bandwidth and server resources.
Q10. When should you NOT use useEffect?
Why interviewers ask: Overusing useEffect is one of the most common React anti-patterns. This tests your understanding of React's data flow.
Model answer: Four cases where useEffect is wrong: (1) Derived values — if you're computing filtered items from a list, just compute it during render or use useMemo. Using useEffect causes an unnecessary extra render. (2) Event responses — if you need to send analytics when a button is clicked, do it in the click handler, not in an effect watching a "submitted" state variable. (3) Resetting state on prop change — use a key prop on the component to force a remount with fresh state instead of using useEffect to reset manually. (4) App initialisation — code that runs once on app start (analytics init, config loading) should be outside components, not in an effect. The general rule: useEffect is for synchronising with external systems. If you're just transforming data or responding to user events, there's a better tool.
Q11. Explain setState batching in class components vs React 18.
Why interviewers ask: Tests understanding of React's update mechanism and asynchronous behaviour.
Model answer: setState is asynchronous — it queues an update rather than immediately changing this.state. React batches multiple setState calls within the same synchronous execution context into a single re-render for performance. In React 17 and earlier, batching only worked inside React event handlers — calls inside setTimeout, fetch.then, or native event listeners were NOT batched, causing multiple re-renders. React 18 introduced automatic batching — all setState calls are batched regardless of where they originate. This means even setTimeout(() => { setState(a); setState(b); }) produces one render in React 18, whereas it produced two in React 17. If you ever need to force a synchronous update in React 18, you can use flushSync().
Q12. How do you handle cleanup for multiple independent side effects?
Why interviewers ask: Tests code organisation and understanding of separation of concerns.
Model answer: In class components, all side effects are lumped together in componentDidMount and cleaned up in componentWillUnmount, making the code hard to follow. With hooks, you use separate useEffect calls for each independent concern. Each effect has its own dependency array and cleanup function. For example: one effect for a WebSocket connection, another for document title, another for analytics. This is powerful because each effect can be independently tested, has clear dependencies, and can be extracted into a custom hook. The effects run in declaration order and clean up in the same order. This separation makes the code more maintainable and eliminates the class-component problem of unrelated logic interleaved in a single method.
Advanced (Q13–Q18)
Q13. Compare the mental models of class lifecycle methods vs useEffect.
Why interviewers ask: This distinguishes developers who truly understand hooks from those who just use them.
Model answer: Class components think in terms of time — "when this component mounts, do X; when it updates, check if Y changed; when it unmounts, clean up Z." This scatters related logic across three methods. useEffect thinks in terms of synchronisation — "keep this effect in sync with these values." All the logic for a single concern (setup, update response, cleanup) lives in one place. The class model maps to lifecycle events; the hooks model maps to data dependencies. This means useEffect naturally handles both mount and update in one place — there's no separate "first render" vs "subsequent render" concept. The cleanup function replaces both "cleanup before re-run" and "cleanup on unmount." The mental shift is from "when does this happen?" to "what does this synchronise with?"
Q14. Build a complete production error boundary with retry, logging, and render-prop fallback.
Why interviewers ask: Tests real-world implementation skills and understanding of the error boundary pattern in depth.
Model answer: A production error boundary needs: (1) getDerivedStateFromError to switch to error state and show fallback UI immediately, (2) componentDidCatch to log to an error service like Sentry with the component stack trace, (3) a reset mechanism — a method that clears the error state and lets the user retry, (4) a flexible fallback — accept either a React element or a render function that receives { error, reset } so the parent can customise the error UI, (5) key-based reset — accept a resetKey prop so the boundary automatically resets when navigation changes. The important architectural point is to use multiple error boundaries — wrap each independent feature section rather than the entire app, so a crash in the sidebar doesn't take down the main content.
Q15. How would you implement shouldComponentUpdate behaviour with hooks?
Why interviewers ask: Tests understanding of rendering optimisation across both paradigms.
Model answer: In functional components, React.memo is the equivalent of shouldComponentUpdate. Wrapping a component with React.memo(Component) makes it skip re-rendering when its props haven't changed (using shallow comparison). For custom comparison logic, pass a second argument: React.memo(Component, (prevProps, nextProps) => prevProps.id === nextProps.id). Note the comparison function returns true to SKIP re-render (opposite of shouldComponentUpdate which returns true to ALLOW). For state-based optimisation, structure your state so unrelated updates don't trigger re-renders — split state into multiple useState calls or use useMemo for expensive derived computations. Unlike PureComponent, React.memo only compares props, not state.
Q16. Explain getSnapshotBeforeUpdate with a real use case.
Why interviewers ask: This is a niche but important method that shows deep lifecycle knowledge.
Model answer: getSnapshotBeforeUpdate(prevProps, prevState) runs after render() but before the DOM is actually updated. It lets you capture information about the DOM that's about to change. The return value is passed as the third argument to componentDidUpdate. The classic use case is preserving scroll position in a chat log — when new messages are added, you capture the scroll position before the update, then in componentDidUpdate you adjust the scroll to maintain the user's reading position. Without this, adding content above the scroll position would push the user's view downward. There's no direct hook equivalent; you'd need to combine useRef to store pre-update values and useLayoutEffect for post-update adjustments, but it's less elegant because there's no guarantee the DOM hasn't already been painted between measurement and adjustment.
Q17. What is the data fetching evolution in React (useEffect → React Query → Server Components)?
Why interviewers ask: Tests awareness of the ecosystem and architectural thinking about data loading.
Model answer: Phase 1: useEffect + fetch — Manual loading/error states, no caching, no deduplication, race condition bugs, waterfall loading patterns. Works but requires significant boilerplate. Phase 2: TanStack Query / SWR — Declarative data fetching with automatic caching, background refetching, retry logic, optimistic updates, request deduplication, and pagination. Eliminates boilerplate and handles edge cases. Still client-side — user sees a loading state. Phase 3: Server Components (Next.js) — Data is fetched on the server at build or request time. The component receives data as props — no loading state, no client-side fetch, no waterfall. The HTML arrives fully populated. Phase 4: React 19 use() hook — Lets components read promises during render, working with Suspense for a streaming loading experience. The evolution shows React moving data fetching closer to the rendering pipeline for better UX and DX.
Q18. Design a useEffect-based WebSocket hook with reconnection, typing indicators, and message history.
Why interviewers ask: Tests ability to manage complex state and multiple side effects in a real-world scenario.
Model answer: The hook manages several concerns: (1) Connection lifecycle — connect on mount, disconnect on unmount, with the WebSocket instance stored in a useRef to persist across renders. (2) Reconnection — use exponential backoff with a max attempts limit, stored in refs (not state) since they don't need to trigger re-renders. Clear reconnection timers in cleanup. (3) Message handling — parse incoming messages and dispatch to the appropriate state updater based on message type (chat message, typing indicator, presence). (4) Typing indicators — debounce outgoing "typing" events using useRef for the timer, and auto-expire incoming typing indicators after a timeout. (5) Separation — split into multiple useEffect calls: one for connection lifecycle, one for sending messages, one for typing indicator debouncing. The key architectural decision is what goes in state (messages, typing users, connection status) vs refs (WebSocket instance, timers, attempt count).