Topic 4 of 15 · React Developer

Topic 4 : Lifecycle Methods

Lesson TL;DRTopic 4: Lifecycle Methods 📖 6 min read · 🎯 beginner · 🧭 Prerequisites: reactstructure, reactcomponents ⚠️ Note: this lesson was regenerated because the source content did not match the title. Why ...
6 min read·beginner·react · lifecycle · hooks · useeffect

Topic 4: Lifecycle Methods

📖 6 min read · 🎯 beginner · 🧭 Prerequisites: react-structure, react-components

⚠️ Note: this lesson was regenerated because the source content did not match the title.

Why this matters

Here's the thing — when you first start building React components, it feels like they just appear on the screen. But a lot is actually happening behind the scenes. Every component goes through a predictable journey: it mounts, it updates when data changes, and eventually it unmounts and disappears. Understanding this lifecycle is what lets you control when your code runs — when to fetch data from an API, when to set up a timer, and crucially, when to clean up after yourself so you don't leave memory leaks behind.

What You'll Learn

  • Identify the three phases of a React component's lifecycle: mounting, updating, and unmounting
  • Use the key class-component lifecycle methods: componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate, getDerivedStateFromProps, and getSnapshotBeforeUpdate
  • Implement useEffect to replicate lifecycle behavior in functional components
  • Handle errors with componentDidCatch and getDerivedStateFromError

The Analogy

Think of a React component like a Vizag apartment. When a tenant moves in, the landlord runs an inspection and sets everything up — that's mounting. While the tenant lives there, things change: furniture is rearranged, appliances are swapped — that's updating. When the tenant moves out, the landlord cleans up, cancels subscriptions, and hands the keys back — that's unmounting. React's lifecycle methods are the landlord's checklist for each of those moments, guaranteeing nothing is left running after the tenant is gone and nothing is skipped when they arrive.

Chapter 1: The Three Phases

Every React component travels through a predictable sequence of phases from birth to removal.

flowchart LR
    A([Mount]) --> B([Update])
    B --> B
    B --> C([Unmount])
PhaseTriggerPurpose
MountingComponent appears in the DOM for the first timeInitial render, data fetching, subscriptions
UpdatingProps or state changeRe-render, syncing side effects
UnmountingComponent is removed from the DOMCleanup: timers, subscriptions, listeners

React exposes hooks into each phase through lifecycle methods (class components) and the useEffect hook (functional components).

Chapter 2: Class Component Lifecycle Methods

Mounting Phase

When a class component mounts, React calls these methods in order:

  1. constructor(props) — initialize state, bind methods
  2. static getDerivedStateFromProps(props, state) — sync state from props before first render
  3. render() — return JSX
  4. componentDidMount() — runs after the component is inserted into the DOM

componentDidMount is the go-to place for API calls, DOM measurements, and starting subscriptions.

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

  componentDidMount() {
    fetch(`/api/users/${this.props.userId}`)
      .then((res) => res.json())
      .then((user) => this.setState({ user, loading: false }));
  }

  render() {
    const { user, loading } = this.state;
    if (loading) return <p>Loading...</p>;
    return <h1>{user.name}</h1>;
  }
}

Updating Phase

When props or state change, React calls:

  1. static getDerivedStateFromProps(props, state) — runs on every render
  2. shouldComponentUpdate(nextProps, nextState) — return false to bail out of re-render (performance optimization)
  3. render()
  4. getSnapshotBeforeUpdate(prevProps, prevState) — captures DOM info (e.g., scroll position) before the DOM is updated
  5. componentDidUpdate(prevProps, prevState, snapshot) — runs after the DOM has updated
class ScrollableList extends React.Component {
  listRef = React.createRef();

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

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <ul ref={this.listRef}>
        {this.props.items.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    );
  }
}

shouldComponentUpdate lets you skip a re-render when you know nothing meaningful changed:

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.userId !== this.props.userId;
}

Unmounting Phase

React calls one method when a component is removed:

  • componentWillUnmount() — cancel timers, unsubscribe from services, remove event listeners
class LiveTicker extends React.Component {
  componentDidMount() {
    this.intervalId = setInterval(() => {
      this.setState({ time: new Date().toLocaleTimeString() });
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.intervalId); // prevents memory leak
  }

  render() {
    return <p>Current time: {this.state?.time}</p>;
  }
}

getDerivedStateFromProps in Detail

This is a static method that returns an object to merge into state, or null to update nothing. It runs before every single render — both mounting and updating.

class TemperatureDisplay extends React.Component {
  state = { celsius: 0 };

  static getDerivedStateFromProps(props, state) {
    if (props.fahrenheit !== undefined) {
      return { celsius: ((props.fahrenheit - 32) * 5) / 9 };
    }
    return null;
  }

  render() {
    return <p>{this.state.celsius.toFixed(1)}°C</p>;
  }
}

Chapter 3: Error Boundaries

React provides two lifecycle methods for catching errors in the component tree below a class component.

  • static getDerivedStateFromError(error) — update state to show a fallback UI
  • componentDidCatch(error, info) — log the error to a reporting service
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.error('Caught error:', error, info.componentStack);
  }

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

Wrap any subtree with <ErrorBoundary> to prevent a single component crash from breaking the whole app:

<ErrorBoundary>
  <UserProfile userId={42} />
</ErrorBoundary>

Chapter 4: useEffect — Lifecycle in Functional Components

Functional components use the useEffect hook to replicate all three lifecycle phases. Its signature is:

useEffect(() => {
  // side effect code (componentDidMount + componentDidUpdate)
  return () => {
    // cleanup (componentWillUnmount)
  };
}, [dependencies]);

The dependency array controls when the effect runs:

Dependency arrayWhen effect runs
OmittedAfter every render
[] (empty)Once after mount only
[a, b]After mount, and whenever a or b changes

Equivalent of componentDidMount

import { useEffect, useState } from 'react';

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

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser);
  }, []); // empty array = run once after mount

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Equivalent of componentDidUpdate

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

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser);
  }, [userId]); // re-runs whenever userId changes

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Equivalent of componentWillUnmount (Cleanup)

function LiveTicker() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);

    return () => clearInterval(id); // cleanup on unmount
  }, []);

  return <p>Current time: {time}</p>;
}

Multiple useEffect Calls

You can (and should) split unrelated side effects into separate useEffect calls rather than cramming everything into one:

function Dashboard({ userId, theme }) {
  useEffect(() => {
    document.title = `Dashboard — User ${userId}`;
  }, [userId]);

  useEffect(() => {
    document.body.classList.toggle('dark', theme === 'dark');
    return () => document.body.classList.remove('dark');
  }, [theme]);
}

Chapter 5: Putting It Together

Here is a complete functional component that demonstrates mounting, updating, and unmounting lifecycle patterns side-by-side:

import { useState, useEffect } from 'react';

function PostViewer({ postId }) {
  const [post, setPost] = useState(null);
  const [viewCount, setViewCount] = useState(0);

  // Fetch post data whenever postId changes (mount + update)
  useEffect(() => {
    let cancelled = false;
    fetch(`/api/posts/${postId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) setPost(data);
      });
    return () => {
      cancelled = true; // prevent state update on unmounted component
    };
  }, [postId]);

  // Track view count on a timer — clean up on unmount
  useEffect(() => {
    const id = setInterval(() => {
      setViewCount((c) => c + 1);
    }, 5000);
    return () => clearInterval(id);
  }, []);

  if (!post) return <p>Loading post...</p>;

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <small>Views this session: {viewCount}</small>
    </article>
  );
}

The cancelled flag pattern prevents a React warning ("Can't perform a state update on an unmounted component") when an async fetch resolves after the component has already unmounted.

🧪 Try It Yourself

Task: Build a CountdownTimer component that counts down from 10 to 0, then displays "Time's up!" and stops the interval.

Success criteria: You should see the counter tick down in the browser every second and stop cleanly at 0 without any console warnings.

Starter snippet:

import { useState, useEffect } from 'react';

function CountdownTimer() {
  const [count, setCount] = useState(10);

  useEffect(() => {
    if (count === 0) return; // TODO: handle the done state

    const id = setInterval(() => {
      setCount((c) => c - 1);
    }, 1000);

    return () => clearInterval(id); // cleanup
  }, [count]);

  return <h2>{count > 0 ? `${count}...` : "Time's up!"}</h2>;
}

export default CountdownTimer;

Mount this component in App.jsx and watch it run. Then try adding a "Restart" button that resets count to 10.

🔍 Checkpoint Quiz

Q1. In what order does React call class component lifecycle methods during the mounting phase?

A) renderconstructorcomponentDidMount B) constructorgetDerivedStateFromPropsrendercomponentDidMount C) componentDidMountconstructorrender D) constructorrendergetDerivedStateFromPropscomponentDidMount

Q2. What does this useEffect do?

useEffect(() => {
  document.title = `Hello, ${name}`;
}, [name]);

A) Updates the page title once when the component mounts, then never again B) Updates the page title after every render regardless of what changed C) Updates the page title after mount and every time name changes D) Causes an infinite re-render loop

Q3. Given this class component, what happens when the component unmounts?

class Poller extends React.Component {
  componentDidMount() {
    this.timerId = setInterval(this.poll, 3000);
  }
  poll = () => fetch('/api/data').then(console.log);
  render() { return <div />; }
}

A) The interval is automatically cleared by React B) The interval keeps running and fires fetch every 3 seconds forever — a memory leak C) React throws an error because componentWillUnmount is missing D) The interval pauses and resumes when the component remounts

Q4. How would you use useEffect to log a message only once when a component first appears on screen — and not on any subsequent re-render?

A1. B — React calls constructor first, then getDerivedStateFromProps (to sync props into state), then render to produce JSX, and finally componentDidMount once the DOM node exists.

A2. C — The dependency array [name] tells React to re-run the effect after mount and whenever name changes. It will not run on other state or prop changes.

A3. B — Because componentWillUnmount is never defined, clearInterval is never called, so the interval keeps firing even after the component is removed from the DOM. This leaks memory and can cause state-update warnings.

A4. Pass an empty dependency array: useEffect(() => { console.log('mounted'); }, []). The empty array means "no dependencies ever change," so React only runs the effect once after the initial mount.

🪞 Recap

  • React components go through three lifecycle phases: mounting, updating, and unmounting.
  • Class components use named methods (componentDidMount, componentDidUpdate, componentWillUnmount, etc.) to hook into each phase.
  • useEffect in functional components covers all three phases: the effect body handles mount/update, and the returned cleanup function handles unmount.
  • The useEffect dependency array controls when the effect re-runs — empty array for mount-only, specific values for targeted updates.
  • componentDidCatch and getDerivedStateFromError enable error boundaries that catch crashes in the component subtree.

📚 Further Reading

Like this topic? It’s one of 15 in React Developer.

Block your seat for ₹2,500 and join the next cohort.