React Component Lifecycle: Essential Knowledge You Need to Master

VnnTools

Imagine each component in your React application as a "living being". It is "born" (initialized and added to the UI), "grows up" (updates when new data arrives), and eventually "disappears" (when no longer needed). This entire process is called the Component Lifecycle.

React Component Lifecycle: Essential Knowledge You Need to Master

Understanding this lifecycle isn't just theoretical knowledge - it's the key to having complete control over your application, from fetching data at the right time, optimizing performance, to cleaning up resources to prevent memory leaks. This article will guide you through each phase, from the classical concepts in Class Components to the modern era of Hooks. 🚀

What is React Lifecycle? A Visual Perspective

React Lifecycle is a sequence of methods (or Hooks) that are automatically called in a specific order throughout a component's lifetime. It provides us with "anchor points" to execute code at crucial moments.

A component's lifecycle is divided into three main phases:

What is React Component Lifecycle?

  1. Mounting: The phase when a component is created, initial state and props values are set up, and it gets "attached" to the browser's DOM tree. This is when the component first appears on screen.
  2. Updating: This phase occurs when the component's state or props change. React will re-render the component to reflect these changes in the user interface.
  3. Unmounting: The final phase, when the component is removed from the DOM tree. This is the last opportunity to perform cleanup actions.

Understanding these three phases helps you answer important questions like: "Where should I call APIs to fetch data?", "How do I optimize re-rendering?", "When do I need to cancel a subscription?".

Class Components vs Functional Components (with Hooks)

React's development history has created two main approaches to working with lifecycle.

The "Classical" Era - Class Components

Before Hooks were introduced, lifecycle was managed through special methods in Class Components. While rarely used when writing new code today, you'll still encounter them frequently in legacy projects.

The most important methods include:

  • constructor(): Where state is initialized and methods are bound. Called first, only once.
  • render(): The only required method. It reads this.props and this.state to return JSX, describing what the UI should look like.
  • componentDidMount(): Called immediately after the component is rendered and mounted to the DOM. This is the ideal place to call APIs, set up subscriptions, or interact with the DOM.
  • componentDidUpdate(prevProps, prevState): Called immediately after the component is updated (re-rendered). Not called on the first render. Useful for executing side effects when props or state change.
  • componentWillUnmount(): Called immediately before the component is removed from the DOM. This is where cleanup is mandatory: cancel timers, unsubscribe, cancel pending network requests...

Example with Class Component:

import React, { Component } from 'react'

class MyClock extends Component {
  constructor(props) {
    super(props)
    this.state = { date: new Date() }
    console.log('1. Constructor: Initialize state')
  }

  componentDidMount() {
    console.log('3. ComponentDidMount: Mounted to DOM, start timer')
    this.timerID = setInterval(() => this.tick(), 1000)
  }

  componentWillUnmount() {
    console.log('4. ComponentWillUnmount: Clean up timer')
    clearInterval(this.timerID)
  }

  tick() {
    this.setState({ date: new Date() })
  }

  render() {
    console.log('2. Render: Draw the interface')
    return <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  }
}

The "Modern" Era - Functional Components and Hooks

With the introduction of React Hooks, how we interact with lifecycle has changed dramatically. Instead of multiple different methods, we now primarily use a single, extremely powerful Hook: useEffect.

useEffect allows you to perform "side effects" in functional components. It can take on the role of componentDidMount, componentDidUpdate, and componentWillUnmount combined.

Syntax: useEffect(callback, [dependencies])

  • callback: A function containing the side effect code.
  • [dependencies] (dependency array): Determines when the callback will run again. This is the "brain" of useEffect.

How useEffect maps to lifecycle phases:

  1. Mimicking componentDidMount (Run once on initialization):

    • Use an empty dependency array [].
    • When to use: Call APIs to fetch initial data.
useEffect(() => {
  console.log('Component has been mounted to DOM')
  // Call API here
}, []) // Empty array -> runs only once
  1. Mimicking componentDidUpdate (Run when there are changes):

    • Provide state or props values in the dependency array. The effect will run again whenever any of these values change.
    • When to use: Re-call API when an ID changes, update UI based on new props.
useEffect(() => {
  console.log(`User ID has changed to: ${userId}`)
  // Call API to fetch new user info with userId
}, [userId]) // Runs again whenever userId changes
  1. Mimicking componentWillUnmount (Cleanup function):

    • Return a function from inside the useEffect callback. This returned function will be called before the component is unmounted, or before the effect runs again.
    • When to use: Very important! Use to cancel subscriptions, timers, event listeners...
useEffect(() => {
  // Set up the task
  const timerId = setInterval(myFunction, 1000)

  // Cleanup function
  return () => {
    console.log('Cleaning up timer')
    clearInterval(timerId)
  }
}, [])

Real-world Examples and Use Cases

Understanding theory is one thing, applying it in practice is what matters.

Example 1: Fetching Data from API

This is the most common use case for useEffect.

import React, { useState, useEffect } from 'react'

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

  useEffect(() => {
    setLoading(true)
    fetch(`https://api.example.com/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        setUser(data)
        setLoading(false)
      })

    // This cleanup function is useful if the component unmounts
    // before fetch completes, helping avoid setState on unmounted component errors.
    return () => {
      // You can use AbortController to cancel the request here
    }
  }, [userId]) // Refetch data whenever userId changes

  if (loading) return <p>Loading...</p>
  if (!user) return <p>User not found.</p>

  return <h1>{user.name}</h1>
}

Example 2: Listening to Browser Events

For example, tracking window size for responsive UI.

import React, { useState, useEffect } from 'react'

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)

    // Register event listener when component mounts
    window.addEventListener('resize', handleResize)
    console.log("Registered 'resize' event listener")

    // Unregister when component unmounts
    return () => {
      window.removeEventListener('resize', handleResize)
      console.log("Removed 'resize' event listener")
    }
  }, []) // Only needs to run once

  return <p>Window width: {width}px</p>
}

🧠 Tips and Pitfalls to Avoid

  1. ⚠️ Infinite Loop: The most common pitfall with useEffect. If you setState inside an effect without providing a dependency array, or depend on the state itself without a stopping condition, you'll create an infinite loop: render -> effect -> setState -> render...

    • How to avoid: Always provide a dependency array. Think carefully about what you put in this array.
  2. Always have cleanup functions: If you've set up something that needs to be "cancelled" (timer, subscription, event listener), always return a cleanup function in useEffect. This helps prevent memory leaks and unexpected errors.

  3. 💡 Dependency array rules: Include all values (props, state, functions) from outside that your effect uses in the dependency array. The ESLint plugin react-hooks/exhaustive-deps will help you automatically check this.

Conclusion: Mastering React's "Rhythm"

React Lifecycle isn't a dry concept. It's the rhythm, the flow of your application. By mastering the Mounting, Updating, and Unmounting phases - and more importantly, how to control them through useEffect - you'll transition from someone who "writes" React to someone who "architects" effective, powerful, and bug-free React applications.

Think of useEffect as a versatile companion that helps you execute the right code at the right time. Master it, and you've mastered one of the most core and beautiful aspects of React.

Related Posts

React Lazy Loading: An Extremely Effective Technique for Accelerating Web Applications

Learn React Lazy Loading to improve application performance, reduce page load time, and optimize user experience. Detailed guide for ReactJS developers.

What is a React Component? A Detailed Guide for Beginners

Understanding React Components is the key to building modern web application interfaces. Read now to master common component types and how to create your first component.

Everything About Redux in React: Definition, Principles and Detailed Examples

Explore the power of Redux in React. Learn about its working principles, core components and practical examples to help you master State Management.

What is React Router? Complete Guide for Beginners

What is React Router? Learn how React Router helps you build smooth single-page applications (SPAs). Detailed guide from basic installation to advanced features.