Introduction
React is a powerful JavaScript library for building user interfaces, and it provides a well-defined lifecycle for each component. Understanding the React component lifecycle methods is crucial for managing a component’s behavior, optimizing performance, and handling side effects like API calls or event listeners.
State vs. Props: What’s the Difference?
In this article, we will explore:
✅ What the React component lifecycle is
✅ The different phases of a React component
✅ Lifecycle methods in class components
✅ Equivalent lifecycle methods in functional components (React Hooks)
✅ Best practices for using lifecycle methods effectively
By the end of this guide, you will have a clear understanding of how React components work internally and how to use lifecycle methods efficiently. 🚀
1. What is the React Component Lifecycle?
The React component lifecycle is the sequence of stages a component goes through from its creation to destruction. Every component in React follows this lifecycle, which consists of three main phases:
1️⃣ Mounting Phase – When a component is created and inserted into the DOM.
2️⃣ Updating Phase – When a component’s state or props change.
3️⃣ Unmounting Phase – When a component is removed from the DOM.
Each phase has specific lifecycle methods that allow us to execute code at different points in the component’s life.
2. Lifecycle Methods in Class Components
In class components, React provides built-in lifecycle methods that we can override to control component behavior.
2.1 The Three Phases and Their Lifecycle Methods
Phase | Lifecycle Method | Purpose |
---|---|---|
Mounting | constructor() | Initialize state & bind methods |
static getDerivedStateFromProps() | Sync state with props before rendering | |
render() | Return JSX to render UI | |
componentDidMount() | Execute code after component mounts (e.g., API calls) | |
Updating | static getDerivedStateFromProps() | Update state before re-rendering |
shouldComponentUpdate() | Optimize re-renders | |
render() | Re-render UI | |
getSnapshotBeforeUpdate() | Capture info before DOM update | |
componentDidUpdate() | Execute code after an update | |
Unmounting | componentWillUnmount() | Cleanup tasks before component removal |
3. Mounting Phase (When a Component is Created)
This phase occurs when a component is first added to the DOM. The following methods run in sequence:
3.1 constructor()
– Initializing State
The constructor()
method is the first method called when the component is created. It is commonly used to initialize state and bind event handlers.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 }; // Initial state
}
render() {
return <h1>Count: {this.state.count}</h1>;
}
}
✅ Avoid side effects (like API calls) in constructor()
since it should only initialize state.
3.2 static getDerivedStateFromProps()
– Sync State with Props
This method updates state based on changes in props before rendering.
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.newValue !== prevState.value) {
return { value: nextProps.newValue };
}
return null; // No update needed
}
✅ Used rarely, mainly when state depends on props.
3.3 render()
– Displaying UI
This method returns JSX to render the component.
render() {
return <h1>Hello, World!</h1>;
}
✅ Must be a pure function (no API calls or state updates inside render()
).
3.4 componentDidMount()
– Running Code After Mounting
This method runs after the component is rendered, making it the best place for API calls, subscriptions, or timers.
componentDidMount() {
console.log("Component mounted!");
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => this.setState({ data }));
}
✅ Best place for API calls or DOM manipulations.
4. Updating Phase (When a Component’s State or Props Change)
4.1 shouldComponentUpdate()
– Preventing Unnecessary Re-renders
This method controls whether a component should re-render based on state or prop changes.
shouldComponentUpdate(nextProps, nextState) {
return nextState.count !== this.state.count; // Only re-render if count changes
}
✅ Improves performance by reducing unnecessary renders.
4.2 getSnapshotBeforeUpdate()
– Capturing Info Before DOM Changes
This method is used to capture values before the DOM updates (e.g., scroll position).
getSnapshotBeforeUpdate(prevProps, prevState) {
return { scrollPosition: window.scrollY };
}
✅ Rarely used, mostly for handling UI behaviors.
4.3 componentDidUpdate()
– Running Code After an Update
Runs after state or props change, making it useful for fetching new data when props update.
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}
✅ Used for fetching new data when props change.
5. Unmounting Phase (When a Component is Removed)
5.1 componentWillUnmount()
– Cleaning Up Before Removal
This method runs just before a component is removed from the DOM, making it ideal for cleaning up event listeners, timers, or subscriptions.
componentWillUnmount() {
console.log("Component is being removed");
clearInterval(this.timer);
}
✅ Avoid memory leaks by removing event listeners or stopping API calls.
6. React Hooks: Lifecycle in Functional Components
With the introduction of React Hooks, functional components can also handle lifecycle events using the useEffect()
hook.
6.1 Replacing componentDidMount()
useEffect(() => {
console.log("Component mounted!");
}, []);
6.2 Replacing componentDidUpdate()
useEffect(() => {
console.log("Component updated!");
}, [count]); // Runs when count changes
6.3 Replacing componentWillUnmount()
useEffect(() => {
const timer = setInterval(() => console.log("Running..."), 1000);
return () => clearInterval(timer); // Cleanup function
}, []);
✅ The return function in useEffect
acts as componentWillUnmount()
.
7. Best Practices for Using Lifecycle Methods
✅ Use Functional Components with Hooks
Why:
Functional components are simpler, easier to read, and promote cleaner logic with Hooks like useState
, useEffect
, etc. They eliminate the need for boilerplate like this
, class constructors, and binding methods.
Benefits:
- Cleaner syntax
- Easier to test and reuse logic (via custom hooks)
- Encouraged by modern React standards
Example:
// ✅ Functional component with Hooks
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}
Compare with class component:
// ❌ More boilerplate
class Counter extends React.Component {
state = { count: 0 };
render() {
return (
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Clicked {this.state.count} times
</button>
);
}
}
✅ Avoid API Calls Inside render()
Why:
The render()
function (or the JSX returned from a functional component) should be pure — no side effects like API calls or state updates. Making API calls in render causes infinite loops or performance issues.
Correct Pattern:
Use lifecycle methods like:
componentDidMount()
(for class components)useEffect()
(for functional components)
Example using useEffect
:
useEffect(() => {
fetchData();
}, []); // Runs once on mount
❌ Don’t do this:
function MyComponent() {
fetch('/api/data') // This will run on every render! Bad.
return <div>...</div>;
}
✅ Optimize Performance with shouldComponentUpdate()
Why:
In class components, shouldComponentUpdate(nextProps, nextState)
can prevent unnecessary re-renders by telling React whether the component actually needs to re-render.
In Functional Components:
Use React.memo()
and useMemo()
/ useCallback()
for similar optimizations.
Example (Class component):
shouldComponentUpdate(nextProps) {
return nextProps.value !== this.props.value; // Re-render only if props change
}
Example (Functional):
const MyComponent = React.memo(({ value }) => {
return <div>{value}</div>;
});
✅ Clean Up in componentWillUnmount()
Why:
When a component is removed from the DOM, you should:
- Clear timers (e.g.
setTimeout
,setInterval
) - Remove event listeners
- Cancel API requests if needed
This prevents memory leaks and unintended behavior.
Class component:
componentWillUnmount() {
clearInterval(this.timer);
window.removeEventListener('resize', this.handleResize);
}
Functional component (with useEffect
cleanup):
useEffect(() => {
const timer = setInterval(() => { ... }, 1000);
return () => {
clearInterval(timer); // ✅ cleanup
};
}, []);
✅ Use getDerivedStateFromProps()
Sparingly
Why:getDerivedStateFromProps(nextProps, prevState)
is a static method used to update state when props change. But it’s hard to debug, can cause unexpected re-renders, and often leads to anti-patterns like syncing props to state unnecessarily.
When to use:
Only when the component’s internal state truly depends on a change in props and can’t be handled another way.
Bad (anti-pattern):
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.value !== prevState.value) {
return { value: nextProps.value }; // syncing props to state = 👎
}
return null;
}
Better alternatives:
- Use
useEffect()
to watch for prop changes in functional components. - Restructure your component so it doesn’t need internal state duplication.
Want examples comparing class vs functional for all of these? Or maybe some performance tips using tools like React Profiler or memoization?
Conclusion
In this guide, we explored React’s component lifecycle methods, their purpose, and how they map to functional components using Hooks.
✅ Mounting Phase: constructor()
, getDerivedStateFromProps()
, render()
, componentDidMount()
✅ Updating Phase: shouldComponentUpdate()
, getSnapshotBeforeUpdate()
, componentDidUpdate()
✅ Unmounting Phase: componentWillUnmount()
✅ Hooks Equivalent: useEffect()
replaces multiple lifecycle methods
By mastering these lifecycle methods, you can build optimized, efficient, and well-structured React applications. 🚀