DEV Community

Munna Thakur
Munna Thakur

Posted on

Understanding React Class Component Lifecycle: A Deep Dive with Real-World Examples


Even though modern React codebases have shifted toward Hooks, understanding class components remains crucial for any serious React developer. Here's why: if you truly grasp class component lifecycle methods, you'll automatically understand when React renders, when side effects run, how updates are controlled, and why hooks behave the way they do.

This isn't just another definition list. We'll explore each lifecycle method with practical context, real-world use cases, and direct comparisons to their functional equivalents.

The Three Phases of React Component Lifecycle

Every React class component goes through three distinct phases:

  1. Mounting – When the component is created and inserted into the DOM
  2. Updating – When props or state change, triggering a re-render
  3. Unmounting – When the component is removed from the DOM

There's also a fourth phase for Error Handling, which we'll cover at the end.


Phase 1: Mounting (Birth of a Component)

Mounting happens exactly once—when your component first appears on screen.

Execution order:

constructorgetDerivedStateFromPropsrendercomponentDidMount

1. constructor(props)

This is where everything begins. The constructor creates your component instance and sets up initial state.

constructor(props) {
  super(props);
  this.state = {
    count: props.initialCount || 0
  };
}
Enter fullscreen mode Exit fullscreen mode

What you should do here:

  • Initialize state
  • Bind event handlers (if not using arrow functions)

What you shouldn't do:

  • Make API calls
  • Set up subscriptions
  • Cause side effects

Functional equivalent:

const [count, setCount] = useState(initialCount || 0);
Enter fullscreen mode Exit fullscreen mode

2. static getDerivedStateFromProps(props, state)

This static method runs before every render and lets you sync state with incoming props.

static getDerivedStateFromProps(props, state) {
  if (props.resetCount && state.count !== 0) {
    return { count: 0 };
  }
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Return values:

  • object → merges into state
  • null → no state update

Important: This is a static method, so you don't have access to this. It's also considered an anti-pattern in most cases—React team recommends avoiding it unless absolutely necessary.

Functional equivalent:

There isn't a direct one, but you'd typically handle this with useEffect:

useEffect(() => {
  if (resetCount) {
    setCount(0);
  }
}, [resetCount]);
Enter fullscreen mode Exit fullscreen mode

3. render()

The heart of every React component. This method describes what your UI should look like.

render() {
  return <div>{this.state.count}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Rules:

  • Must be pure (no side effects)
  • No setState calls
  • No direct DOM manipulation
  • Must return JSX or null

Functional equivalent:

The entire function body is essentially the render method:

return <div>{count}</div>;
Enter fullscreen mode Exit fullscreen mode

4. componentDidMount()

This runs immediately after your component is inserted into the DOM. It's the perfect place for side effects.

componentDidMount() {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => this.setState({ data }));

  this.timer = setInterval(() => {
    this.setState({ time: new Date() });
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

Common use cases:

  • API calls
  • Setting up subscriptions
  • Starting timers
  • Direct DOM manipulation (if needed)

Functional equivalent:

useEffect(() => {
  // Your side effects here
}, []); // Empty dependency array = runs once on mount
Enter fullscreen mode Exit fullscreen mode

Phase 2: Updating (Component Re-renders)

Updates happen when state or props change.

Execution order:

getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate

5. shouldComponentUpdate(nextProps, nextState)

This is your performance optimization checkpoint. It lets you prevent unnecessary re-renders.

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.id !== this.props.id || 
         nextState.count !== this.state.count;
}
Enter fullscreen mode Exit fullscreen mode

Return values:

  • true → proceed with render
  • false → skip render (and all subsequent lifecycle methods)

Functional equivalent:

const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id; // true = skip render
});
Enter fullscreen mode Exit fullscreen mode

6. getSnapshotBeforeUpdate(prevProps, prevState)

This rarely-used method captures information from the DOM right before React applies updates.

getSnapshotBeforeUpdate(prevProps, prevState) {
  if (prevProps.list.length < this.props.list.length) {
    const list = this.listRef.current;
    return list.scrollHeight - list.scrollTop;
  }
  return null;
}
Enter fullscreen mode Exit fullscreen mode

The returned value gets passed to componentDidUpdate as the third argument.

Functional equivalent:

useLayoutEffect(() => {
  // Runs synchronously after DOM mutations
}, [dependencies]);
Enter fullscreen mode Exit fullscreen mode

7. componentDidUpdate(prevProps, prevState, snapshot)

Runs after the component updates in the DOM. Perfect for reacting to prop or state changes.

componentDidUpdate(prevProps) {
  if (prevProps.userId !== this.props.userId) {
    this.fetchUserData(this.props.userId);
  }
}
Enter fullscreen mode Exit fullscreen mode

Critical rule: Always compare previous values before calling setState, or you'll create an infinite loop.

Functional equivalent:

useEffect(() => {
  fetchUserData(userId);
}, [userId]); // Runs when userId changes
Enter fullscreen mode Exit fullscreen mode

Phase 3: Unmounting (Cleanup)

8. componentWillUnmount()

This is your cleanup phase. It runs right before the component is destroyed.

componentWillUnmount() {
  clearInterval(this.timer);
  this.subscription.unsubscribe();
  document.removeEventListener('scroll', this.handleScroll);
}
Enter fullscreen mode Exit fullscreen mode

Common cleanup tasks:

  • Clear timers
  • Cancel network requests
  • Remove event listeners
  • Unsubscribe from services

Functional equivalent:

useEffect(() => {
  const timer = setInterval(() => {}, 1000);

  return () => {
    clearInterval(timer); // Cleanup function
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Error Handling Phase

9. static getDerivedStateFromError(error)

Updates state when a child component throws an error, allowing you to render a fallback UI.

static getDerivedStateFromError(error) {
  return { hasError: true };
}
Enter fullscreen mode Exit fullscreen mode

10. componentDidCatch(error, info)

Logs error details for monitoring and debugging.

componentDidCatch(error, info) {
  console.error('Error caught:', error);
  console.error('Component stack:', info.componentStack);
  // Send to error reporting service
}
Enter fullscreen mode Exit fullscreen mode

Note: There's no direct hook equivalent for error boundaries yet. You still need class components for this.


Real-World Example: User Profile with Full Lifecycle

Let's build a component that demonstrates multiple lifecycle methods working together:

class UserProfile extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      loading: true,
      error: null
    };
  }

  componentDidMount() {
    this.fetchUser(this.props.userId);
  }

  componentDidUpdate(prevProps) {
    // Fetch new user data when userId prop changes
    if (prevProps.userId !== this.props.userId) {
      this.setState({ loading: true });
      this.fetchUser(this.props.userId);
    }
  }

  componentWillUnmount() {
    // Cancel any pending requests
    if (this.controller) {
      this.controller.abort();
    }
  }

  fetchUser(userId) {
    this.controller = new AbortController();

    fetch(`/api/users/${userId}`, { 
      signal: this.controller.signal 
    })
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(user => this.setState({ 
        user, 
        loading: false, 
        error: null 
      }))
      .catch(error => {
        if (error.name !== 'AbortError') {
          this.setState({ 
            error: error.message, 
            loading: false 
          });
        }
      });
  }

  render() {
    const { loading, error, user } = this.state;

    if (loading) return <div className="spinner">Loading...</div>;
    if (error) return <div className="error">Error: {error}</div>;
    if (!user) return null;

    return (
      <div className="user-profile">
        <img src={user.avatar} alt={user.name} />
        <h2>{user.name}</h2>
        <p>{user.email}</p>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

What this demonstrates:

  • constructor sets initial state
  • componentDidMount fetches data on first render
  • componentDidUpdate handles prop changes intelligently
  • componentWillUnmount prevents memory leaks
  • render stays pure and simple

Key Takeaways

Understanding class component lifecycle methods gives you:

Better debugging skills – You'll know exactly when and why components re-render

Deeper React mental model – Hooks aren't magic, they're lifecycle methods in disguise

Legacy codebase confidence – Many production apps still use class components

Performance optimization knowledge – You'll understand when to use memo, useMemo, and useCallback

Hooks didn't replace lifecycle concepts—they just changed the API. The underlying principles remain the same.

Whether you're maintaining legacy code or just want to truly understand React, mastering lifecycle methods is time well spent.


Have you worked with class components in production? What lifecycle method patterns have you found most useful? Drop your experiences in the comments!

Top comments (0)