Learn the concept
Hooks
useEffect with an empty dependency array replaces componentDidMount, useEffect with dependencies replaces componentDidUpdate, and the useEffect cleanup function replaces componentWillUnmount. However, hooks represent a different mental model focused on synchronizing with external systems rather than responding to lifecycle events.
Class component lifecycle methods and hooks solve the same problems but with fundamentally different mental models. Class lifecycles are organized around when things happen (mount, update, unmount), while hooks are organized around what you're synchronizing with (data, subscriptions, DOM).
| Class Lifecycle | Hook Equivalent |
|----------------|----------------|
| constructor | useState initial value or useRef |
| componentDidMount | useEffect(fn, []) |
| componentDidUpdate | useEffect(fn, [deps]) |
| componentWillUnmount | useEffect cleanup function |
| shouldComponentUpdate | React.memo or useMemo |
| getDerivedStateFromProps | Compute during render (no hook needed) |
| getSnapshotBeforeUpdate | No direct hook equivalent |
| componentDidCatch | No hook equivalent (use class Error Boundaries) |
// Class
componentDidMount() {
fetchData();
document.title = 'Loaded';
}
// Hook — empty dependency array = run once after first render
useEffect(() => {
fetchData();
document.title = 'Loaded';
}, []);// Class
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
fetchUser(this.props.userId);
}
}
// Hook — runs when userId changes
useEffect(() => {
fetchUser(userId);
}, [userId]);The hook version is simpler: you declare what you depend on, and React handles when to re-run.
// Class
componentWillUnmount() {
socket.disconnect();
clearInterval(this.timerId);
}
// Hook — return a cleanup function from useEffect
useEffect(() => {
const timerId = setInterval(tick, 1000);
socket.connect();
return () => {
clearInterval(timerId);
socket.disconnect();
};
}, []);The cleanup function also runs before the effect re-runs (on dependency changes), not just on unmount. This prevents stale subscriptions.
The key insight is that hooks don't map 1:1 to lifecycle methods. A single useEffect can combine mount + update + unmount logic for one concern, while class components scatter related logic across multiple lifecycle methods:
// Class — related logic split across 3 methods
componentDidMount() { subscribe(this.props.id); }
componentDidUpdate(prev) {
if (prev.id !== this.props.id) {
unsubscribe(prev.id);
subscribe(this.props.id);
}
}
componentWillUnmount() { unsubscribe(this.props.id); }
// Hook — all related logic in one place
useEffect(() => {
subscribe(id);
return () => unsubscribe(id);
}, [id]); // Handles mount, update, AND unmountuseRef + useLayoutEffect as a workaround.useErrorBoundary hook in React (though libraries like react-error-boundary provide one).// CLASS COMPONENT
class UserProfile extends React.Component {
state = { user: null, loading: true };
componentDidMount() {
this.fetchUser();
document.title = 'Profile';
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
document.title = 'App'; // Reset title
}
async fetchUser() {
this.setState({ loading: true });
const res = await fetch(`/api/users/${this.props.userId}`);
const user = await res.json();
this.setState({ user, loading: false });
}
render() {
if (this.state.loading) return <p>Loading...</p>;
return <h1>{this.state.user.name}</h1>;
}
}
// FUNCTIONAL COMPONENT WITH HOOKS
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) {
setUser(data);
setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; }; // Cleanup: prevent stale updates
}, [userId]); // Re-runs when userId changes
useEffect(() => {
document.title = 'Profile';
return () => { document.title = 'App'; }; // Cleanup on unmount
}, []);
if (loading) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}Migrating class components to functional components in existing codebases requires understanding the lifecycle-to-hooks mapping to preserve behavior.
Understanding the hooks mental model helps organize effects by concern (data fetching, subscriptions, DOM manipulation) rather than by timing (mount, update, unmount).
Take a class component with multiple lifecycle methods and refactor it to use hooks. Compare the before/after code and verify identical behavior.