The Component Lifecycle Phases
Think of a component's lifecycle like the life of a plant: it's planted (mounted), grows and changes (updates), and eventually withers (unmounts). React provides us with specific moments where we can interact with components during these phases.
Lifecycle in Class Components
Before hooks, class components were the only way to access lifecycle methods. Let's understand these traditional lifecycle methods:
1. Mounting Phase
class LifecycleDemo extends React.Component {
constructor(props) {
super(props);
console.log('1. Constructor: Component is being created');
this.state = {
data: null,
loading: true
};
}
componentDidMount() {
console.log('3. ComponentDidMount: Component was just mounted');
// Perfect place for API calls, subscriptions
this.fetchData();
}
fetchData = async () => {
try {
const response = await fetch('/api/data');
const data = await response.json();
this.setState({ data, loading: false });
} catch (error) {
this.setState({ error, loading: false });
}
}
render() {
console.log('2. Render: Component is rendering');
const { data, loading, error } = this.state;
if (loading) return Loading...;
if (error) return Error: {error.message};
return (
Data: {data}
);
}
}
2. Updating Phase
class UpdateDemo extends React.Component {
state = {
count: 0,
lastUpdate: null
};
shouldComponentUpdate(nextProps, nextState) {
console.log('Should Update?: Deciding whether to re-render');
// Optimize by preventing unnecessary renders
return nextState.count !== this.state.count;
}
componentDidUpdate(prevProps, prevState) {
console.log('Did Update: Component just updated');
// Handle side effects after updates
if (prevState.count !== this.state.count) {
this.setState({ lastUpdate: new Date().toLocaleTimeString() });
}
}
increment = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
Count: {this.state.count}
Last updated: {this.state.lastUpdate}
);
}
}
3. Unmounting Phase
class UnmountDemo extends React.Component {
componentDidMount() {
// Set up subscription
this.subscription = subscribeToData(data => {
this.setState({ data });
});
// Start interval
this.interval = setInterval(() => {
this.setState({ time: new Date().toLocaleTimeString() });
}, 1000);
}
componentWillUnmount() {
console.log('Will Unmount: Cleaning up before removal');
// Clean up to prevent memory leaks
this.subscription.unsubscribe();
clearInterval(this.interval);
}
render() {
return Current time: {this.state.time};
}
}
Lifecycle with Hooks
With hooks, we don't have named lifecycle methods, but we can achieve the same functionality:
Mapping Class Lifecycle to Hooks
// Class Component Lifecycle
class LifecycleClass extends React.Component {
state = { data: null };
componentDidMount() {
fetchData().then(data => {
this.setState({ data });
});
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
fetchData(this.props.id).then(data => {
this.setState({ data });
});
}
}
componentWillUnmount() {
cancelDataFetch();
}
render() {
return {this.state.data};
}
}
// Equivalent with Hooks
function LifecycleHooks({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
// componentDidMount & componentDidUpdate
fetchData(id).then(setData);
// componentWillUnmount
return () => {
cancelDataFetch();
};
}, [id]); // Dependencies
return {data};
}
Advanced Lifecycle Patterns
1. Error Boundaries
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
// Update state to show fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught:', error, errorInfo);
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
Something went wrong.
Error Details
{this.state.error.toString()}
);
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<ComponentThatMightError />
</ErrorBoundary>
2. Conditional Effects
function SearchComponent({ query, category }) {
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
// Effect only runs when query changes
useEffect(() => {
if (!query) return;
setIsSearching(true);
searchByQuery(query).then(data => {
setResults(data);
setIsSearching(false);
});
}, [query]);
// Separate effect for category changes
useEffect(() => {
if (!category) return;
filterByCategory(category).then(filtered => {
setResults(filtered);
});
}, [category]);
return (
{isSearching ? (
<LoadingSpinner />
) : (
<ResultsList results={results} />
)}
);
}
3. Lifecycle with Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let mounted = true;
async function fetchUserData() {
try {
setLoading(true);
const [userData, userPosts] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/posts`).then(r => r.json())
]);
if (mounted) {
setUser(userData);
setPosts(userPosts);
setLoading(false);
}
} catch (error) {
if (mounted) {
console.error('Failed to fetch user data:', error);
setLoading(false);
}
}
}
fetchUserData();
return () => {
mounted = false;
};
}, [userId]);
if (loading) return <LoadingSpinner />;
if (!user) return User not found;
return (
<UserInfo user={user} />
<UserPosts posts={posts} />
);
}
Lifecycle Optimization Techniques
1. useMemo for Expensive Computations
function ExpensiveComponent({ items, filter }) {
// Only recompute when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
const totalValue = useMemo(() => {
console.log('Calculating total...');
return filteredItems.reduce((sum, item) => sum + item.value, 0);
}, [filteredItems]);
return (
Total Value: ${totalValue}
{filteredItems.map(item => (
- {item.name}: ${item.value}
))}
);
}
2. useCallback for Stable Functions
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// This function is recreated on every render
const unstableIncrement = () => {
setCount(c => c + 1);
};
// This function is stable across renders
const stableIncrement = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies, so never recreated
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<ChildComponent onIncrement={stableIncrement} />
</div>
);
}
const ChildComponent = React.memo(({ onIncrement }) => {
console.log('Child rendered');
return ;
});
Common Lifecycle Patterns
1. Auto-save with Debounce
function AutoSaveEditor() {
const [content, setContent] = useState('');
const [saveStatus, setSaveStatus] = useState('saved');
useEffect(() => {
if (!content) return;
setSaveStatus('saving...');
const timeoutId = setTimeout(() => {
saveToServer(content).then(() => {
setSaveStatus('saved');
});
}, 1000); // Debounce delay
return () => clearTimeout(timeoutId);
}, [content]);
return (
<div>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
placeholder="Start typing..."
/>
<div>Status: {saveStatus}</div>
</div>
);
}
2. Window Event Listeners
function WindowEventComponent() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
function handleScroll() {
setScrollPosition(window.pageYOffset);
}
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
Window: {windowSize.width} x {windowSize.height}
Scroll: {scrollPosition}px
);
}
3. Intersection Observer
function LazyImage({ src, alt }) {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
{isVisible ? (
) : (
Loading...
)}
);
}
Lifecycle Best Practices
Lifecycle Debugging
function useLifecycleLogger(componentName) {
const mounted = useRef(false);
useEffect(() => {
console.log(`${componentName} mounted`);
mounted.current = true;
return () => {
console.log(`${componentName} will unmount`);
mounted.current = false;
};
}, [componentName]);
useEffect(() => {
if (mounted.current) {
console.log(`${componentName} updated`);
}
});
return mounted;
}
// Usage
function MyComponent(props) {
const mounted = useLifecycleLogger('MyComponent');
return (
{/* component content */}
);
}