Understanding React Class Component Lifecycle Step-by-Step

VirajViraj
8 min read

React's lifecycle methods are fundamental to understanding how components behave and optimize performance. In this comprehensive guide, we'll explore the two critical phases of React's lifecycle, understand component batching, and dive deep into what happens behind the scenes during initial mounting and subsequent updates.

Introduction to React Lifecycle Phases

Every React component goes through a series of phases during its lifetime. Understanding these phases is crucial for writing efficient React applications and debugging complex component behaviors.

React's lifecycle is divided into two main phases:

  1. Render Phase - Pure computation, no side effects

  2. Commit Phase - DOM manipulation and side effects

Let's dive deep into each phase and understand what happens under the hood.

The Two Phases Explained

Render Phase: The Blueprint Creation

The Render Phase is where React prepares the component for display. This phase is pure and has no side effects, meaning React can pause, abort, or restart it for optimization purposes.

What happens in Render Phase:

  1. Component is mounted (instance created)

  2. Constructor is called (for class components)

  3. Render method is executed

  4. Virtual DOM tree is created

  5. Reconciliation occurs (comparing trees)

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    console.log('πŸ”§ Constructor called - Render Phase');
    this.state = { count: 0 };
  }

  render() {
    console.log('🎨 Render method called - Render Phase');
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increment
        </button>
      </div>
    );
  }
}

Commit Phase: Making It Real

The Commit Phase is where React actually updates the DOM and runs side effects. This phase cannot be interrupted and runs synchronously.

What happens in Commit Phase:

  1. React updates the actual DOM

  2. Layout effects run (useLayoutEffect)

  3. componentDidMount is called

  4. Refs are attached

  5. Cleanup functions are scheduled

componentDidMount() {
  console.log('πŸš€ componentDidMount called - Commit Phase');
  // DOM is now updated and visible
  // Safe to make API calls, set up subscriptions, etc.

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

Component Batching and Optimization

One of React's most powerful optimizations is batching. When you have a parent component with multiple children, React doesn't render them one by one. Instead, it batches the process for maximum efficiency.

The Batching Process

Consider this component hierarchy:

Parent
β”œβ”€β”€ Child1
└── Child2

Here's the exact order of execution:

Render Phase (Top-down):

  1. Parent component mounting begins

  2. Parent constructor called

  3. Parent render method called

  4. Child1 mounting begins

  5. Child1 constructor called

  6. Child1 render method called

  7. Child2 mounting begins

  8. Child2 constructor called

  9. Child2 render method called

Commit Phase (Bottom-up): 10. Child1 componentDidMount called 11. Child2 componentDidMount called 12. Parent componentDidMount called

Why This Order Matters

The bottom-up execution in the commit phase ensures that:

  • Child components are fully mounted before parent's componentDidMount

  • Parent can safely access child DOM elements

  • Event handlers and refs work correctly

  • API calls in parent can depend on children being ready

class Parent extends React.Component {
  componentDidMount() {
    // At this point, ALL children are guaranteed to be mounted
    // Safe to query child DOM elements or call child methods
    console.log('Parent mounted - all children are ready!');
  }

  render() {
    return (
      <div>
        <Child1 ref={this.child1Ref} />
        <Child2 ref={this.child2Ref} />
      </div>
    );
  }
}

Virtual DOM vs Real DOM

Understanding the difference between Virtual DOM and Real DOM is crucial for grasping React's performance benefits.

What is Virtual DOM?

Virtual DOM is simply a JavaScript object representation of the actual DOM. When you write JSX, it gets converted into these objects during the Render Phase.

// Your JSX
<div className="container">
  <h1>Hello World</h1>
  <button onClick={handleClick}>Click me</button>
</div>

// Becomes Virtual DOM (JavaScript objects)
{
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: { children: 'Hello World' }
      },
      {
        type: 'button',
        props: { 
          onClick: handleClick,
          children: 'Click me'
        }
      }
    ]
  }
}

The Journey from Code to Screen

Let's trace what happens when the JavaScript engine reads your React code:

  1. JS Engine encounters component β†’ Component instance created

  2. Render Phase begins β†’ Constructor called, render method executed

  3. JSX converted β†’ Virtual DOM objects created (still just JavaScript!)

  4. Reconciliation β†’ React compares old vs new Virtual DOM trees

  5. Commit Phase begins β†’ Real DOM elements created/updated

  6. Browser rendering β†’ Visual changes appear on screen

Important: No HTML/CSS is actually rendered during the Render Phase. Only JavaScript objects are created!

From Virtual to Real

During the Commit Phase, React transforms Virtual DOM into Real DOM:

// Virtual DOM object
{ type: 'div', props: { children: 'Hello' } }

// Becomes Real DOM
const div = document.createElement('div');
div.textContent = 'Hello';
document.body.appendChild(div);

Initial Mount vs Updates

This is where React's efficiency truly shines. The lifecycle behaves differently for initial mounting versus subsequent updates.

Initial Mount: The Complete Journey

When a component is first rendered:

class DataComponent extends React.Component {
  constructor() {
    super();
    this.state = { data: null, loading: true };
    console.log('1. πŸ”§ Constructor - Initial setup');
  }

  render() {
    console.log('2. 🎨 Render - Creating Virtual DOM');
    return (
      <div>
        {this.state.loading ? (
          <div>Loading...</div>
        ) : (
          <div>Data: {JSON.stringify(this.state.data)}</div>
        )}
      </div>
    );
  }

  componentDidMount() {
    console.log('3. πŸš€ componentDidMount - DOM is ready!');

    // API call triggers an update
    fetch('/api/users')
      .then(response => response.json())
      .then(data => {
        console.log('πŸ“‘ API response received, triggering update...');
        this.setState({ data, loading: false });
      });
  }
}

Updates: The Optimized Path

After setState is called, React enters an optimized update cycle:

componentDidUpdate(prevProps, prevState) {
  console.log('4. πŸ”„ componentDidUpdate - Update complete!');
  console.log('Previous state:', prevState);
  console.log('Current state:', this.state);

  // Perfect place for side effects based on state changes
  if (prevState.loading && !this.state.loading) {
    console.log('βœ… Data loading completed!');
  }
}

Key Differences: Mount vs Update

AspectInitial MountUpdate
Constructorβœ… Called❌ Skipped
Renderβœ… Calledβœ… Called
DOM Creationβœ… Creates new elements❌ Updates existing
Lifecycle HookcomponentDidMountcomponentDidUpdate
ReconciliationNot needed (no comparison)βœ… Compares Virtual DOM trees
PerformanceSlower (full creation)Faster (targeted updates)

The Update Reconciliation Process

When setState triggers an update:

  1. New Virtual DOM created with updated state

  2. Reconciliation algorithm compares old vs new trees

  3. Minimal changes calculated (diffing algorithm)

  4. Only changed elements updated in Real DOM

// Before update (loading state)
<div>Loading...</div>

// After setState (data loaded)
<div>Data: {"name": "John", "age": 30}</div>

// React only updates the text content!
// The div element is reused for efficiency

Practical Examples

Let's see these concepts in action with real-world scenarios:

Example 1: Data Fetching Component

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

  async componentDidMount() {
    try {
      const response = await fetch(`/api/users/${this.props.userId}`);
      const user = await response.json();

      // This setState triggers a re-render
      this.setState({ user, loading: false });
    } catch (error) {
      this.setState({ error: error.message, loading: false });
    }
  }

  componentDidUpdate(prevProps) {
    // Handle prop changes (different user ID)
    if (prevProps.userId !== this.props.userId) {
      this.setState({ loading: true });
      this.fetchUserData();
    }
  }

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

    if (loading) return <div>Loading user profile...</div>;
    if (error) return <div>Error: {error}</div>;

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

Example 2: Parent-Child Communication

class TodoApp extends React.Component {
  constructor() {
    super();
    this.state = { todos: [] };
  }

  componentDidMount() {
    console.log('TodoApp mounted - children are ready');
    // All child components (TodoList, AddTodo) are now mounted
    this.loadTodos();
  }

  render() {
    return (
      <div>
        <AddTodo onAdd={this.handleAddTodo} />
        <TodoList 
          todos={this.state.todos} 
          onToggle={this.handleToggleTodo}
        />
      </div>
    );
  }
}

class TodoList extends React.Component {
  componentDidMount() {
    console.log('TodoList mounted first');
    // This runs before parent's componentDidMount
  }

  render() {
    return (
      <ul>
        {this.props.todos.map(todo => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
    );
  }
}

Performance Implications

Understanding React's lifecycle phases helps you write more performant applications:

Optimization Strategies

  1. Minimize Render Phase Work
// ❌ Expensive computation in render
render() {
  const expensiveValue = this.calculateExpensiveValue(); // Bad!
  return <div>{expensiveValue}</div>;
}

// βœ… Memoize expensive computations
constructor() {
  super();
  this.expensiveValue = this.calculateExpensiveValue();
}
  1. Batch State Updates
// ❌ Multiple setState calls
handleMultipleUpdates = () => {
  this.setState({ count: this.state.count + 1 });
  this.setState({ name: 'Updated' });
  this.setState({ active: true });
  // Triggers 3 re-renders
}

// βœ… Single setState call
handleMultipleUpdates = () => {
  this.setState({
    count: this.state.count + 1,
    name: 'Updated',
    active: true
  });
  // Triggers 1 re-render
}
  1. Optimize componentDidUpdate
componentDidUpdate(prevProps, prevState) {
  // ❌ Unconditional side effects
  this.fetchData(); // Infinite loop risk!

  // βœ… Conditional side effects
  if (prevProps.userId !== this.props.userId) {
    this.fetchData();
  }
}

React DevTools Profiler

Use React DevTools Profiler to visualize the render and commit phases:

  • Render phase duration - Time spent in component logic

  • Commit phase duration - Time spent updating DOM

  • Component update reasons - Why components re-rendered

Conclusion

Understanding React's lifecycle phasesβ€”Render and Commitβ€”is fundamental to building efficient React applications. The Render Phase creates the Virtual DOM blueprint, while the Commit Phase brings it to life in the real DOM. React's batching optimization ensures components mount efficiently, and the distinction between initial mounting and updates allows for optimal performance.

Key takeaways:

  • Render Phase is pure and interruptible

  • Commit Phase handles DOM updates and side effects

  • Batching optimizes component tree updates

  • Virtual DOM enables efficient reconciliation

  • Updates skip constructor and only re-render what changed


0
Subscribe to my newsletter

Read articles from Viraj directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Viraj
Viraj