ReactJS - Component Life Cycle

Hello there, future React developers! Today, we're going to embark on an exciting journey through the life cycle of React components. Don't worry if you're new to programming – I'll be your friendly guide, explaining everything step by step. By the end of this tutorial, you'll have a solid understanding of how React components come to life, grow, and eventually say goodbye. Let's dive in!

ReactJS - Component Life Cycle

What is a Component Life Cycle?

Before we jump into the code, let's talk about what we mean by a "life cycle." Just like how we humans go through different stages in life (birth, growth, and, well, you know...), React components also have their own life stages. These stages are called the component life cycle.

Think of a React component as a virtual pet. When you first create it, it's born. Then it grows and changes as you interact with it. Finally, when you don't need it anymore, it goes away. Understanding this life cycle is crucial for creating dynamic and efficient React applications.

The Three Main Phases of a Component's Life

React components go through three main phases:

  1. Mounting (Birth)
  2. Updating (Growth)
  3. Unmounting (Farewell)

Let's explore each of these phases and the methods associated with them.

1. Mounting Phase

This is when our component is born and added to the DOM (Document Object Model – think of it as the family tree of your webpage).

Key methods in this phase:

Method Description
constructor() The component's constructor, called before it is mounted
render() The method that actually renders the component
componentDidMount() Called after the component is mounted to the DOM

Let's see a simple example:

import React, { Component } from 'react';

class BabyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { message: "I'm just born!" };
    console.log("Constructor: Hello, I'm being created!");
  }

  componentDidMount() {
    console.log("componentDidMount: I'm now in the DOM!");
  }

  render() {
    console.log("Render: I'm being rendered!");
    return <h1>{this.state.message}</h1>;
  }
}

export default BabyComponent;

In this example, our BabyComponent goes through its birth phase. The constructor is called first, setting up the initial state. Then render is called to create the actual DOM elements. Finally, componentDidMount is called, signaling that our component is now fully born and added to the DOM.

2. Updating Phase

This phase occurs when a component's state changes or it receives new props.

Key methods in this phase:

Method Description
shouldComponentUpdate() Decides if the component should re-render
render() Re-renders the component with updated data
componentDidUpdate() Called after the component updates

Let's extend our previous example:

import React, { Component } from 'react';

class GrowingComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { age: 0 };
  }

  componentDidMount() {
    this.ageInterval = setInterval(() => {
      this.setState(prevState => ({ age: prevState.age + 1 }));
    }, 1000);
  }

  shouldComponentUpdate(nextProps, nextState) {
    return nextState.age % 5 === 0; // Only update every 5 years
  }

  componentDidUpdate() {
    console.log(`I just turned ${this.state.age}!`);
  }

  render() {
    return <h1>I am {this.state.age} years old</h1>;
  }

  componentWillUnmount() {
    clearInterval(this.ageInterval);
  }
}

export default GrowingComponent;

In this example, our GrowingComponent "ages" every second. The shouldComponentUpdate method ensures we only celebrate birthdays every 5 years (to save on cake, you know). When the component does update, componentDidUpdate logs a birthday message.

3. Unmounting Phase

This is the farewell phase, when a component is removed from the DOM.

Key method in this phase:

Method Description
componentWillUnmount() Called just before the component is unmounted and destroyed

In our GrowingComponent example above, we used componentWillUnmount to clear the interval we set up, preventing any memory leaks.

Working Example of Life Cycle API

Now, let's put it all together in a more complex example. We'll create a simple "Mood Tracker" app that demonstrates all the life cycle methods.

import React, { Component } from 'react';

class MoodTracker extends Component {
  constructor(props) {
    super(props);
    this.state = { mood: 'neutral', intensity: 5 };
    console.log('Constructor: Mood Tracker is being created');
  }

  componentDidMount() {
    console.log('ComponentDidMount: Mood Tracker is now in the DOM');
    this.moodInterval = setInterval(() => {
      const moods = ['happy', 'sad', 'excited', 'nervous', 'neutral'];
      const newMood = moods[Math.floor(Math.random() * moods.length)];
      this.setState({ mood: newMood });
    }, 5000);
  }

  shouldComponentUpdate(nextProps, nextState) {
    return this.state.mood !== nextState.mood;
  }

  componentDidUpdate() {
    console.log(`Mood updated to: ${this.state.mood}`);
  }

  componentWillUnmount() {
    console.log('ComponentWillUnmount: Goodbye Mood Tracker!');
    clearInterval(this.moodInterval);
  }

  handleIntensityChange = (e) => {
    this.setState({ intensity: e.target.value });
  }

  render() {
    console.log('Render: Mood Tracker is rendering');
    return (
      <div>
        <h1>Current Mood: {this.state.mood}</h1>
        <input 
          type="range" 
          min="1" 
          max="10" 
          value={this.state.intensity} 
          onChange={this.handleIntensityChange}
        />
        <p>Intensity: {this.state.intensity}</p>
      </div>
    );
  }
}

export default MoodTracker;

This MoodTracker component demonstrates all the life cycle methods we've discussed. It randomly changes mood every 5 seconds and allows the user to adjust the mood intensity.

Life Cycle API in Expense Manager App

Now, let's consider how we might use these life cycle methods in a practical application like an Expense Manager.

import React, { Component } from 'react';

class ExpenseManager extends Component {
  constructor(props) {
    super(props);
    this.state = { expenses: [], total: 0 };
  }

  componentDidMount() {
    // Simulate fetching expenses from an API
    setTimeout(() => {
      const fetchedExpenses = [
        { id: 1, description: 'Groceries', amount: 50 },
        { id: 2, description: 'Gas', amount: 30 },
        { id: 3, description: 'Movie tickets', amount: 20 },
      ];
      this.setState({ expenses: fetchedExpenses }, this.calculateTotal);
    }, 1000);
  }

  shouldComponentUpdate(nextProps, nextState) {
    // Only update if the total has changed
    return this.state.total !== nextState.total;
  }

  componentDidUpdate() {
    console.log(`Total expenses updated: $${this.state.total}`);
  }

  calculateTotal = () => {
    const total = this.state.expenses.reduce((sum, expense) => sum + expense.amount, 0);
    this.setState({ total });
  }

  render() {
    return (
      <div>
        <h1>Expense Manager</h1>
        <ul>
          {this.state.expenses.map(expense => (
            <li key={expense.id}>{expense.description}: ${expense.amount}</li>
          ))}
        </ul>
        <h2>Total: ${this.state.total}</h2>
      </div>
    );
  }
}

export default ExpenseManager;

In this ExpenseManager component:

  1. We use componentDidMount to simulate fetching expense data from an API when the component first loads.
  2. shouldComponentUpdate ensures we only re-render when the total changes, not for every small update.
  3. componentDidUpdate logs the new total whenever it changes.

By leveraging these life cycle methods, we create a more efficient and responsive expense tracking application.

Understanding the component life cycle is crucial for building efficient React applications. It allows you to control when certain actions occur, optimize performance, and manage resources effectively. Keep practicing with these concepts, and soon you'll be creating complex, dynamic React applications with ease!

Credits: Image by storyset