Skip to main content

component lifecycle methods in React

React component lifecycle methods are predefined functions in class components that React calls automatically at specific points: when a component appears in the DOM, updates, or gets removed.

Theory

TL;DR

  • Three phases: Mounting (component created), Updating (props/state changed), Unmounting (component removed)
  • Mount order: constructorrendercomponentDidMount. Always in that sequence.
  • Side effects like API calls and timers go in componentDidMount, not in render or constructor
  • Hooks replicate all three phases via useEffect with a dependency array
  • New code: use hooks. Existing class components: stick with lifecycle methods

Quick Example

jsx
class Timer extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; console.log('1. Constructor'); } componentDidMount() { // DOM is ready here, safe to start side effects this.timer = setInterval(() => { this.setState({ count: this.state.count + 1 }); }, 1000); console.log('3. DidMount - timer started'); } componentWillUnmount() { clearInterval(this.timer); // prevents memory leak console.log('Unmounting - timer cleared'); } render() { console.log('2. Render'); return <div>Count: {this.state.count}</div>; } } // Output on mount: 1. Constructor → 2. Render → 3. DidMount

constructor runs first, render builds the virtual DOM, then componentDidMount fires after the real DOM is ready. That order is fixed.

The Three Phases

Mounting is when a component first appears in the DOM:

MethodWhen called
constructorComponent instance created, state initialized
static getDerivedStateFromPropsBefore every render (rarely needed)
renderReturns JSX. Must be a pure function.
componentDidMountAfter the DOM is painted and ready

Updating happens when props or state change:

MethodWhen called
static getDerivedStateFromPropsBefore re-render
shouldComponentUpdateCan return false to skip re-render
renderProduces updated JSX
getSnapshotBeforeUpdateReads DOM before update (scroll position, etc.)
componentDidUpdateAfter DOM updates, safe for side effects

Unmounting has one method: componentWillUnmount. It fires before React removes the component. Clean up timers, event listeners, and pending requests here.

Key Difference from Hooks

Class lifecycle methods follow a strict call order enforced by React's Fiber reconciler. render never runs during unmount. componentDidMount always fires after the DOM commit. That predictability makes class components easier to reason about in complex scenarios.

useEffect folds all three phases into one function. A wrong dependency array causes either stale data or an infinite re-render loop. Class methods make that bug impossible by design.

When to Use Each Method

  • Initial data fetch: componentDidMount
  • Re-fetch when a prop changes: componentDidUpdate with a prevProps comparison
  • Skip unnecessary re-renders: shouldComponentUpdate (or React.memo)
  • Cleanup timers and subscriptions: componentWillUnmount
  • Sync state from props: getDerivedStateFromProps (rare, prefer useEffect)
  • Capture scroll before a DOM update: getSnapshotBeforeUpdate

Class Lifecycle vs Hooks

AspectClass LifecycleHooks (useEffect)
PhasesMounting / Updating / Unmounting (9+ methods)Single effect with deps array
Order guaranteeStrict and predictableRuns after render; deps control re-runs
CleanupcomponentWillUnmountReturn function from useEffect
PerformanceshouldComponentUpdate for fine controlReact.memo + useMemo in React 18+
Learning curveMore API surface to memorizeSimpler, but dependency arrays are tricky
When to useLegacy codebasesAll new code

How React Runs This Internally

React's Fiber reconciler builds a work-in-progress tree during reconciliation. It can pause and resume that work to prioritize user input over background updates. After the tree commits to the real DOM, React fires componentDidMount or componentDidUpdate. Unmounting triggers componentWillUnmount before the tree is deleted. In React 18's concurrent mode, effects batch and defer, which is part of why hooks behave slightly differently from class lifecycle methods.

Common Mistakes

API call in render:

jsx
// Wrong - runs on every render, blocks the UI render() { const data = fetch('/api/todos'); return <div>{data}</div>; } // Correct componentDidMount() { fetch('/api/todos') .then(res => res.json()) .then(data => this.setState({ data })); }

render must be pure. No async calls, no side effects.

Missing cleanup in componentWillUnmount:

jsx
componentDidMount() { this.timer = setInterval(tick, 1000); // No cleanup = memory leak on every route change } componentWillUnmount() { clearInterval(this.timer); // This line is the whole point }

Missing cleanup in componentWillUnmount is the most common React memory leak I've seen in production apps. In a React Router setup, components mount and unmount on every navigation, so intervals and subscriptions stack up fast.

Calling setState in componentWillUnmount:

jsx
componentWillUnmount() { this.setState({ done: true }); // Warning: can't update unmounted component }

The component is already gone. React ignores the update and logs a warning. Use a boolean flag checked in componentDidUpdate instead.

No prevProps guard in componentDidUpdate:

jsx
// Wrong - infinite loop componentDidUpdate() { this.fetchData(); // triggers after every update, including updates it causes } // Correct componentDidUpdate(prevProps) { if (this.props.userId !== prevProps.userId) { this.fetchData(); } }

Always guard componentDidUpdate. Without a condition, you get an infinite render loop.

Real-World Usage

  • React Router: componentDidMount fetches route-specific data (user profile, post detail)
  • Redux-Observable: subscribe to epics in componentDidMount, unsubscribe in componentWillUnmount
  • Chat apps: getSnapshotBeforeUpdate captures scroll position, componentDidUpdate restores it after new messages load above the viewport
  • Next.js: componentDidMount runs only on the client, useful for browser-only APIs like localStorage
  • React 18 migration: replacing class lifecycle with useEffect unlocks concurrent rendering features

Follow-up Questions

Q: What is the mounting order when a parent renders a child component?
A: Parent constructor, parent render, child constructor, child render, child componentDidMount, then parent componentDidMount. Children always mount before parents finish mounting.

Q: Why not make API calls in constructor?
A: constructor runs before the component is in the DOM. There are no guarantees about DOM state, and in SSR (Next.js, Remix) it causes hydration mismatches. componentDidMount is the correct place.

Q: What is getSnapshotBeforeUpdate actually used for?
A: It runs right before React applies DOM changes. You can read current DOM values like scroll position and return them to componentDidUpdate as a third argument. Most common use: preserving chat scroll when new messages load above the current view.

Q: Can shouldComponentUpdate introduce bugs?
A: Yes. Returning false when the component should update breaks rendering with no error thrown. The same risk exists with React.memo and a custom comparator. Always profile with React DevTools Profiler before adding this optimization.

Q: In React 18 concurrent mode, can lifecycle methods run more than once?
A: render can be invoked multiple times before a commit. But componentDidMount and componentDidUpdate still fire once per commit. This is a core reason React 18 favors hooks: useEffect has clearer semantics in concurrent scenarios.

Examples

Basic: Tracking Lifecycle Order

jsx
class LifecycleDemo extends React.Component { constructor(props) { super(props); this.state = { name: props.name }; console.log('1. constructor'); } static getDerivedStateFromProps(props, state) { console.log('2. getDerivedStateFromProps'); return null; } componentDidMount() { console.log('4. componentDidMount - DOM ready'); } componentDidUpdate(prevProps, prevState) { console.log('5. componentDidUpdate'); } componentWillUnmount() { console.log('6. componentWillUnmount - cleanup here'); } render() { console.log('3. render'); return <div>Hello, {this.state.name}</div>; } }

Run this in a browser to see the exact order React uses. getDerivedStateFromProps fires before every render, including updates. That surprises most developers the first time.

Intermediate: Todo List with API Fetch

jsx
class TodoList extends React.Component { state = { todos: [], loading: true }; componentDidMount() { this.fetchTodos(); } componentDidUpdate(prevProps) { // Re-fetch only when the filter prop changes if (this.props.filter !== prevProps.filter) { this.fetchTodos(); } } fetchTodos() { this.setState({ loading: true }); fetch(`/api/todos?filter=${this.props.filter}`) .then(res => res.json()) .then(todos => this.setState({ todos, loading: false })); } render() { if (this.state.loading) return <div>Loading...</div>; return ( <ul> {this.state.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } }

componentDidMount handles the initial load. componentDidUpdate handles prop-driven re-fetches without duplicating the fetch logic. The prevProps check prevents an infinite loop.

Advanced: Debounced Search with Cleanup

jsx
class SearchResults extends React.Component { state = { results: [] }; componentDidUpdate(prevProps) { if (this.props.query !== prevProps.query) { this.scheduleFetch(); } } scheduleFetch = () => { // Arrow function keeps 'this' correct clearTimeout(this.timeout); this.timeout = setTimeout(() => { fetch(`/api/search?q=${this.props.query}`) .then(res => res.json()) .then(data => this.setState({ results: data.results })); }, 300); }; componentWillUnmount() { clearTimeout(this.timeout); // cancel pending fetch if user navigates away } render() { return ( <ul> {this.state.results.map(r => <li key={r.id}>{r.title}</li>)} </ul> ); } }

Two things keep this correct. The debounce avoids a request on every keystroke. The cleanup in componentWillUnmount cancels any pending timeout if the component unmounts before the 300ms window closes, preventing a setState call on an unmounted component.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?