React Component Lifecycle Methods (and Their Hooks Equivalents)

Blog / ReactJS · May 21, 2025 · Updated June 10, 2026 · 8 min read
React Component Lifecycle Methods (and Their Hooks Equivalents)

Every React component moves through a lifecycle with three phases: mounting (added to the DOM), updating (re-rendered after a state or prop change), and unmounting (removed from the DOM). Historically you hooked into these phases with class lifecycle methods such as componentDidMount and componentWillUnmount. As of 2026, that approach is legacy: modern React expresses the same lifecycle with hooks in function components — primarily useState for state and useEffect for side effects and cleanup.

Class lifecycle methods are not removed or deprecated in React 18 or 19 — existing class components keep working — but new code is written with function components and hooks. This guide covers each class lifecycle method accurately, then maps every one of them to its hooks equivalent so you can read old code, maintain it, and write the modern version.

The three lifecycle phases

  • Mounting — the component instance is created and inserted into the DOM. This is where you set up initial state, fetch data, and start subscriptions.
  • Updating — the component re-renders because its props or state changed. This is where you respond to changes (e.g. refetch when an id prop changes).
  • Unmounting — the component is removed from the DOM. This is where you clean up: cancel timers, close sockets, and remove event listeners to avoid memory leaks.

A fourth concern, error handling, lets a component catch rendering errors from its children (error boundaries). We cover it at the end because it is the one case that still requires a class today.

Mounting phase methods

During mounting, a class component runs these methods in order: constructorgetDerivedStateFromPropsrendercomponentDidMount.

constructor(props) initialises state and binds event handlers. Always call super(props) first.

constructor(props) {
  super(props);
  this.state = { count: 0 };
  this.handleAdd = this.handleAdd.bind(this);
}

render() is the only required method. It must be pure: read this.props and this.state, return JSX (or null), and never mutate state or cause side effects here.

componentDidMount() runs once, immediately after the component is first inserted into the DOM. This is the correct place to fetch data, set up subscriptions, or start timers. You may call setState here; it triggers an extra render before the browser paints.

Updating phase methods

When props or state change, a class component runs: getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate.

static getDerivedStateFromProps(props, state) is a rarely needed static method that returns an object to update state from props before every render, or null for no change. Because it is static it cannot access this, and it runs on every render — so reach for it only when state must mirror a prop.

shouldComponentUpdate(nextProps, nextState) lets you skip a re-render for performance by returning false. Most teams instead extend React.PureComponent (a shallow prop/state comparison) rather than hand-writing this.

shouldComponentUpdate(nextProps, nextState) {
  // Skip re-render unless count actually changed.
  return nextState.count !== this.state.count;
}

getSnapshotBeforeUpdate(prevProps, prevState) runs right before the DOM is mutated, letting you capture information (such as a scroll position) that you read after the update. Its return value is passed as the third argument to componentDidUpdate.

componentDidUpdate(prevProps, prevState, snapshot) runs after every update (but not the first mount). Compare prevProps/prevState to the current values before acting — and always guard any setState here with a condition, or you create an infinite loop.

componentDidUpdate(prevProps) {
  // Refetch only when the id prop actually changes.
  if (prevProps.id !== this.props.id) {
    this.fetchData(this.props.id);
  }
}

Unmounting phase

componentWillUnmount() runs once, just before the component is removed from the DOM. Clean up everything you created in componentDidMount: clear timers, cancel in-flight requests, unsubscribe from stores, and remove event listeners. Skipping this is the most common source of memory leaks and "setState on an unmounted component" warnings.

A note on the deprecated UNSAFE_ methods

Three older lifecycle methods are now legacy and unsafe for React's concurrent rendering. They are aliased with an UNSAFE_ prefix and should not be used in new code:

  • UNSAFE_componentWillMount (was componentWillMount)
  • UNSAFE_componentWillReceiveProps (was componentWillReceiveProps)
  • UNSAFE_componentWillUpdate (was componentWillUpdate)

They can fire multiple times under concurrent features and lead to subtle bugs. Their use cases are covered by getDerivedStateFromProps, getSnapshotBeforeUpdate, and — far more commonly — by hooks.

Mapping class lifecycle methods to hooks

This is the part that matters in 2026. Hooks do not replace lifecycle methods one-for-one — instead you think in terms of what state you need (useState) and which side effects should run, and when (useEffect). One useEffect with a dependency array and an optional cleanup function covers mount, update, and unmount together.

Class lifecycle method Hooks equivalent
constructor (set initial state) useState(initialValue)
componentDidMount useEffect(() => { ... }, [])
componentDidUpdate useEffect(() => { ... }, [deps])
componentWillUnmount cleanup returned from useEffect: return () => { ... }
shouldComponentUpdate / PureComponent React.memo + useMemo / useCallback
getDerivedStateFromProps derive value during render, or useState + reset via key
getSnapshotBeforeUpdate useLayoutEffect (reads layout before paint)
render the function body's return
componentDidCatch / getDerivedStateFromError error boundary (class) or the react-error-boundary package

Key points: a useEffect with an empty dependency array [] behaves like componentDidMount plus componentWillUnmount; with dependencies it adds componentDidUpdate behaviour, re-running only when a listed value changes. Use useLayoutEffect (not useEffect) when you must read or write the DOM before the browser paints, which is the getSnapshotBeforeUpdate use case.

Side by side: a class component and its hooks rewrite

Here is a small component that fetches data on mount, refetches when its userId prop changes, and cleans up on unmount. First the class version, using componentDidMount, componentDidUpdate, and componentWillUnmount.

import React, { Component } from 'react';

class UserProfile extends Component {
  constructor(props) {
    super(props);
    this.state = { user: null };
    this.controller = null;
  }

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

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.loadUser(this.props.userId);
    }
  }

  componentWillUnmount() {
    // Cleanup: cancel any in-flight request.
    this.controller?.abort();
  }

  async loadUser(userId) {
    this.controller?.abort();
    this.controller = new AbortController();
    const res = await fetch(`/api/users/${userId}`, { signal: this.controller.signal });
    this.setState({ user: await res.json() });
  }

  render() {
    const { user } = this.state;
    return <div>{user ? user.name : 'Loading...'}</div>;
  }
}

export default UserProfile;

Now the same component rewritten with hooks. useState replaces the constructor's state, and a single useEffect keyed on userId handles the initial fetch, the refetch on prop change, and the cleanup — the work of all three class methods above.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUser() {
      const res = await fetch(`/api/users/${userId}`, { signal: controller.signal });
      setUser(await res.json());
    }
    loadUser();

    // Cleanup runs before the next effect and on unmount.
    return () => controller.abort();
  }, [userId]); // re-runs whenever userId changes

  return <div>{user ? user.name : 'Loading...'}</div>;
}

export default UserProfile;

Notice how the hooks version co-locates the setup and its cleanup in one place, instead of spreading the same concern across componentDidMount, componentDidUpdate, and componentWillUnmount. That co-location is the main reason hooks are easier to maintain. If you are still learning the basics of components and where state lives, our guide to React component, state and props is a good companion to this one.

Error boundaries: the one method without a hook

There is still no hook for catching render errors. To build an error boundary you implement a class with static getDerivedStateFromError(error) (to render a fallback UI) and/or componentDidCatch(error, info) (to log the error). In practice most teams use the well-maintained react-error-boundary package, which wraps this class for you and exposes a clean component API.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true }; // render the fallback UI
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) return <h2>Something went wrong.</h2>;
    return this.props.children;
  }
}

Practical guidance

  • Write new components as functions with hooks. Reserve classes for error boundaries (or legacy code you cannot yet migrate).
  • Think in effects, not phases. Ask "what should this effect synchronise with?" and list those values as dependencies, rather than translating method-by-method.
  • Always clean up. Return a cleanup function from useEffect for every subscription, timer, or listener.
  • Avoid premature optimisation. Reach for React.memo, useMemo, and useCallback only when you have a measured render problem — not as a default replacement for shouldComponentUpdate.
  • Don't fear class code. Plenty of production React still uses lifecycle methods. The mapping table above lets you read it confidently and migrate incrementally.

Migrating a large class-based React codebase to hooks, or building a new app on React 18/19, is the kind of work our team does day to day — see our React development services if you'd like an experienced hand. If you're weighing React for a cross-platform build, our React Native vs Flutter comparison and React Router navigation guide are useful next reads.

Frequently Asked Questions

Are React lifecycle methods deprecated?

No. As of 2026, class lifecycle methods like componentDidMount and componentWillUnmount still work in React 18 and 19 and are not scheduled for removal. What changed is the recommendation: new components are written as functions with hooks, so lifecycle methods are now considered legacy rather than deprecated. Three older methods (componentWillMount, componentWillReceiveProps, componentWillUpdate) are the exception — they are renamed with an UNSAFE_ prefix and should not be used.

What replaced componentDidMount?

useEffect with an empty dependency array: useEffect(() => { /* run once after mount */ }, []). The effect runs after the component is first painted, which is where you fetch data or set up subscriptions — exactly what componentDidMount was for. If you return a function from that effect, it runs on unmount, covering componentWillUnmount too.

How do hooks handle the updating phase (componentDidUpdate)?

Add dependencies to useEffect: useEffect(() => { ... }, [id]) re-runs the effect whenever id changes, which mirrors componentDidUpdate with a prevProps comparison. You no longer write the if (prevProps.id !== this.props.id) guard by hand — the dependency array does it for you.

What is the difference between useEffect and useLayoutEffect?

Both run effects after render, but useLayoutEffect runs synchronously after the DOM is mutated and before the browser paints, while useEffect runs after paint. Use useLayoutEffect when you must read layout (like a scroll position or element size) and adjust it before the user sees a flicker — this is the hooks equivalent of getSnapshotBeforeUpdate. For everything else, prefer useEffect.

Can I create an error boundary with hooks?

Not directly — there is still no hook for catching render errors. You implement an error boundary as a class component using static getDerivedStateFromError and componentDidCatch, or you use the react-error-boundary package, which wraps that class and gives you a hook-friendly API for resetting and handling errors.

Should I rewrite all my class components as hooks?

Not all at once. Existing class components keep working, so migrate incrementally — typically when you're already touching a component for other reasons. Write all new components with hooks, and keep classes only where they're still required, such as error boundaries. The lifecycle-to-hooks mapping table above is the practical reference for each migration.

Share this article