Component Lifecycle
Component Lifecycle
React components go through a lifecycle of events from creation to destruction. Understanding these lifecycle phases helps you control component behavior and optimize performance.
Lifecycle in Class Components
Class components have explicit lifecycle methods divided into three phases:
Mounting Phase
When a component is being created and inserted into the DOM:
class LifecycleDemo extends React.Component {
constructor(props) {
super(props);
console.log('1. Constructor called');
this.state = { count: 0 };
}
static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps called');
// Return new state or null
return null;
}
componentDidMount() {
console.log('4. componentDidMount called');
// Good place for:
// - API calls
// - Subscriptions
// - Timers
}
render() {
console.log('3. Render called');
return <div>Count: {this.state.count}</div>;
}
}
Updating Phase
When props or state changes:
class UpdateDemo extends React.Component {
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps (update)');
return null;
}
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate');
// Return true to update, false to prevent update
return true;
}
render() {
console.log('Render (update)');
return <div>{this.props.value}</div>;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate');
// Capture information from DOM before update
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('componentDidUpdate');
// Good place for:
// - DOM operations
// - Network requests based on prop changes
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}
}
Unmounting Phase
When a component is being removed from the DOM:
class UnmountDemo extends React.Component {
componentDidMount() {
this.timer = setInterval(() => {
console.log('Timer running');
}, 1000);
}
componentWillUnmount() {
console.log('componentWillUnmount called');
// Clean up:
// - Timers
// - Subscriptions
// - Pending requests
clearInterval(this.timer);
}
render() {
return <div>Component with timer</div>;
}
}
Lifecycle in Functional Components (Hooks)
Functional components use hooks to replicate lifecycle behavior:
useEffect Hook
Combines componentDidMount, componentDidUpdate, and componentWillUnmount:
import React, { useState, useEffect } from 'react';
function LifecycleWithHooks() {
const [count, setCount] = useState(0);
// componentDidMount & componentDidUpdate
useEffect(() => {
console.log('Effect ran');
// componentWillUnmount
return () => {
console.log('Cleanup');
};
}); // Runs after every render
// componentDidMount only
useEffect(() => {
console.log('Mounted');
return () => {
console.log('Unmounting');
};
}, []); // Empty dependency array
// componentDidUpdate for specific values
useEffect(() => {
console.log('Count changed:', count);
}, [count]); // Only when count changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Common Lifecycle Patterns
Data Fetching
// Class Component
class UserProfile extends React.Component {
state = { user: null, loading: true };
componentDidMount() {
this.fetchUser();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
fetchUser = async () => {
this.setState({ loading: true });
try {
const response = await fetch(`/api/users/${this.props.userId}`);
const user = await response.json();
this.setState({ user, loading: false });
} catch (error) {
this.setState({ loading: false, error });
}
}
render() {
const { user, loading } = this.state;
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
}
// Functional Component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
Subscriptions
// Class Component
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove);
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove);
}
handleMouseMove = (e) => {
this.setState({ x: e.clientX, y: e.clientY });
}
render() {
return <div>Mouse: {this.state.x}, {this.state.y}</div>;
}
}
// Functional Component
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty array = only on mount/unmount
return <div>Mouse: {position.x}, {position.y}</div>;
}
Timers
// Class Component
class Timer extends React.Component {
state = { seconds: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState(state => ({ seconds: state.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <div>Seconds: {this.state.seconds}</div>;
}
}
// Functional Component
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Seconds: {seconds}</div>;
}
Advanced Patterns
Previous Value Hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Debounced Effect
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const timeoutId = setTimeout(() => {
if (query) {
// Perform search
performSearch(query).then(setResults);
}
}, 500); // Debounce delay
return () => clearTimeout(timeoutId);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}
Layout Effect
function Tooltip({ text, targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
if (targetRef.current) {
const rect = targetRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 5,
left: rect.left
});
}
}, [targetRef]);
return (
<div
style={{
position: 'absolute',
top: position.top,
left: position.left
}}
>
{text}
</div>
);
}
Error Boundaries
Handle errors in component lifecycle:
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
// Update state to show error UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
Performance Optimization
shouldComponentUpdate
class OptimizedComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Only update if specific props change
return (
this.props.value !== nextProps.value ||
this.state.count !== nextState.count
);
}
render() {
return <div>{this.props.value}</div>;
}
}
React.memo for Functional Components
const OptimizedComponent = React.memo(function Component({ value }) {
console.log('Rendering');
return <div>{value}</div>;
}, (prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.value === nextProps.value;
});
Best Practices
- Clean up side effects - Always return cleanup functions
- Handle race conditions - Cancel outdated requests
- Avoid memory leaks - Clear timers and subscriptions
- Use dependency arrays correctly - Include all dependencies
- Prefer hooks over class components for new code
- Don't overuse effects - Consider if you really need them
- Keep effects focused - One effect per concern
Common Pitfalls
Missing Dependencies
// ❌ Missing dependency
useEffect(() => {
setCount(count + 1);
}, []); // Should include 'count'
// ✅ Correct
useEffect(() => {
setCount(c => c + 1);
}, []);
Not Cleaning Up
// ❌ Memory leak
useEffect(() => {
const timer = setInterval(() => {
console.log('Running');
}, 1000);
// Missing cleanup!
});
// ✅ Proper cleanup
useEffect(() => {
const timer = setInterval(() => {
console.log('Running');
}, 1000);
return () => clearInterval(timer);
}, []);
Understanding component lifecycle is crucial for building robust React applications. Master these concepts to handle side effects properly and optimize performance!