Understanding React Class Component Lifecycle Step-by-Step

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:
Render Phase - Pure computation, no side effects
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:
Component is mounted (instance created)
Constructor is called (for class components)
Render method is executed
Virtual DOM tree is created
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:
React updates the actual DOM
Layout effects run (useLayoutEffect)
componentDidMount
is calledRefs are attached
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):
Parent component mounting begins
Parent constructor called
Parent render method called
Child1 mounting begins
Child1 constructor called
Child1 render method called
Child2 mounting begins
Child2 constructor called
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:
JS Engine encounters component β Component instance created
Render Phase begins β Constructor called, render method executed
JSX converted β Virtual DOM objects created (still just JavaScript!)
Reconciliation β React compares old vs new Virtual DOM trees
Commit Phase begins β Real DOM elements created/updated
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
Aspect | Initial Mount | Update |
Constructor | β Called | β Skipped |
Render | β Called | β Called |
DOM Creation | β Creates new elements | β Updates existing |
Lifecycle Hook | componentDidMount | componentDidUpdate |
Reconciliation | Not needed (no comparison) | β Compares Virtual DOM trees |
Performance | Slower (full creation) | Faster (targeted updates) |
The Update Reconciliation Process
When setState
triggers an update:
New Virtual DOM created with updated state
Reconciliation algorithm compares old vs new trees
Minimal changes calculated (diffing algorithm)
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
- 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();
}
- 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
}
- 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
Subscribe to my newsletter
Read articles from Viraj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
