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.
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:
- 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.
- 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.
- 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 readsthis.props
andthis.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 thecallback
will run again. This is the "brain" ofuseEffect
.
How useEffect
maps to lifecycle phases:
-
Mimicking
componentDidMount
(Run once on initialization):- Use an empty dependency array
[]
. - When to use: Call APIs to fetch initial data.
- Use an empty dependency array
useEffect(() => {
console.log('Component has been mounted to DOM')
// Call API here
}, []) // Empty array -> runs only once
-
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
-
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...
- Return a function from inside the
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
-
⚠️ Infinite Loop: The most common pitfall with
useEffect
. If yousetState
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.
-
✅ 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. -
💡 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.