component lifecycle methods in React
React component lifecycle methods are predefined functions in class components that React calls automatically at specific points: when a component appears in the DOM, updates, or gets removed.
Theory
TL;DR
- Three phases: Mounting (component created), Updating (props/state changed), Unmounting (component removed)
- Mount order:
constructor→render→componentDidMount. Always in that sequence. - Side effects like API calls and timers go in
componentDidMount, not inrenderorconstructor - Hooks replicate all three phases via
useEffectwith a dependency array - New code: use hooks. Existing class components: stick with lifecycle methods
Quick Example
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
console.log('1. Constructor');
}
componentDidMount() {
// DOM is ready here, safe to start side effects
this.timer = setInterval(() => {
this.setState({ count: this.state.count + 1 });
}, 1000);
console.log('3. DidMount - timer started');
}
componentWillUnmount() {
clearInterval(this.timer); // prevents memory leak
console.log('Unmounting - timer cleared');
}
render() {
console.log('2. Render');
return <div>Count: {this.state.count}</div>;
}
}
// Output on mount: 1. Constructor → 2. Render → 3. DidMountconstructor runs first, render builds the virtual DOM, then componentDidMount fires after the real DOM is ready. That order is fixed.
The Three Phases
Mounting is when a component first appears in the DOM:
| Method | When called |
|---|---|
constructor | Component instance created, state initialized |
static getDerivedStateFromProps | Before every render (rarely needed) |
render | Returns JSX. Must be a pure function. |
componentDidMount | After the DOM is painted and ready |
Updating happens when props or state change:
| Method | When called |
|---|---|
static getDerivedStateFromProps | Before re-render |
shouldComponentUpdate | Can return false to skip re-render |
render | Produces updated JSX |
getSnapshotBeforeUpdate | Reads DOM before update (scroll position, etc.) |
componentDidUpdate | After DOM updates, safe for side effects |
Unmounting has one method: componentWillUnmount. It fires before React removes the component. Clean up timers, event listeners, and pending requests here.
Key Difference from Hooks
Class lifecycle methods follow a strict call order enforced by React's Fiber reconciler. render never runs during unmount. componentDidMount always fires after the DOM commit. That predictability makes class components easier to reason about in complex scenarios.
useEffect folds all three phases into one function. A wrong dependency array causes either stale data or an infinite re-render loop. Class methods make that bug impossible by design.
When to Use Each Method
- Initial data fetch:
componentDidMount - Re-fetch when a prop changes:
componentDidUpdatewith aprevPropscomparison - Skip unnecessary re-renders:
shouldComponentUpdate(orReact.memo) - Cleanup timers and subscriptions:
componentWillUnmount - Sync state from props:
getDerivedStateFromProps(rare, preferuseEffect) - Capture scroll before a DOM update:
getSnapshotBeforeUpdate
Class Lifecycle vs Hooks
| Aspect | Class Lifecycle | Hooks (useEffect) |
|---|---|---|
| Phases | Mounting / Updating / Unmounting (9+ methods) | Single effect with deps array |
| Order guarantee | Strict and predictable | Runs after render; deps control re-runs |
| Cleanup | componentWillUnmount | Return function from useEffect |
| Performance | shouldComponentUpdate for fine control | React.memo + useMemo in React 18+ |
| Learning curve | More API surface to memorize | Simpler, but dependency arrays are tricky |
| When to use | Legacy codebases | All new code |
How React Runs This Internally
React's Fiber reconciler builds a work-in-progress tree during reconciliation. It can pause and resume that work to prioritize user input over background updates. After the tree commits to the real DOM, React fires componentDidMount or componentDidUpdate. Unmounting triggers componentWillUnmount before the tree is deleted. In React 18's concurrent mode, effects batch and defer, which is part of why hooks behave slightly differently from class lifecycle methods.
Common Mistakes
API call in render:
// Wrong - runs on every render, blocks the UI
render() {
const data = fetch('/api/todos');
return <div>{data}</div>;
}
// Correct
componentDidMount() {
fetch('/api/todos')
.then(res => res.json())
.then(data => this.setState({ data }));
}render must be pure. No async calls, no side effects.
Missing cleanup in componentWillUnmount:
componentDidMount() {
this.timer = setInterval(tick, 1000);
// No cleanup = memory leak on every route change
}
componentWillUnmount() {
clearInterval(this.timer); // This line is the whole point
}Missing cleanup in componentWillUnmount is the most common React memory leak I've seen in production apps. In a React Router setup, components mount and unmount on every navigation, so intervals and subscriptions stack up fast.
Calling setState in componentWillUnmount:
componentWillUnmount() {
this.setState({ done: true }); // Warning: can't update unmounted component
}The component is already gone. React ignores the update and logs a warning. Use a boolean flag checked in componentDidUpdate instead.
No prevProps guard in componentDidUpdate:
// Wrong - infinite loop
componentDidUpdate() {
this.fetchData(); // triggers after every update, including updates it causes
}
// Correct
componentDidUpdate(prevProps) {
if (this.props.userId !== prevProps.userId) {
this.fetchData();
}
}Always guard componentDidUpdate. Without a condition, you get an infinite render loop.
Real-World Usage
- React Router:
componentDidMountfetches route-specific data (user profile, post detail) - Redux-Observable: subscribe to epics in
componentDidMount, unsubscribe incomponentWillUnmount - Chat apps:
getSnapshotBeforeUpdatecaptures scroll position,componentDidUpdaterestores it after new messages load above the viewport - Next.js:
componentDidMountruns only on the client, useful for browser-only APIs likelocalStorage - React 18 migration: replacing class lifecycle with
useEffectunlocks concurrent rendering features
Follow-up Questions
Q: What is the mounting order when a parent renders a child component?
A: Parent constructor, parent render, child constructor, child render, child componentDidMount, then parent componentDidMount. Children always mount before parents finish mounting.
Q: Why not make API calls in constructor?
A: constructor runs before the component is in the DOM. There are no guarantees about DOM state, and in SSR (Next.js, Remix) it causes hydration mismatches. componentDidMount is the correct place.
Q: What is getSnapshotBeforeUpdate actually used for?
A: It runs right before React applies DOM changes. You can read current DOM values like scroll position and return them to componentDidUpdate as a third argument. Most common use: preserving chat scroll when new messages load above the current view.
Q: Can shouldComponentUpdate introduce bugs?
A: Yes. Returning false when the component should update breaks rendering with no error thrown. The same risk exists with React.memo and a custom comparator. Always profile with React DevTools Profiler before adding this optimization.
Q: In React 18 concurrent mode, can lifecycle methods run more than once?
A: render can be invoked multiple times before a commit. But componentDidMount and componentDidUpdate still fire once per commit. This is a core reason React 18 favors hooks: useEffect has clearer semantics in concurrent scenarios.
Examples
Basic: Tracking Lifecycle Order
class LifecycleDemo extends React.Component {
constructor(props) {
super(props);
this.state = { name: props.name };
console.log('1. constructor');
}
static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps');
return null;
}
componentDidMount() {
console.log('4. componentDidMount - DOM ready');
}
componentDidUpdate(prevProps, prevState) {
console.log('5. componentDidUpdate');
}
componentWillUnmount() {
console.log('6. componentWillUnmount - cleanup here');
}
render() {
console.log('3. render');
return <div>Hello, {this.state.name}</div>;
}
}Run this in a browser to see the exact order React uses. getDerivedStateFromProps fires before every render, including updates. That surprises most developers the first time.
Intermediate: Todo List with API Fetch
class TodoList extends React.Component {
state = { todos: [], loading: true };
componentDidMount() {
this.fetchTodos();
}
componentDidUpdate(prevProps) {
// Re-fetch only when the filter prop changes
if (this.props.filter !== prevProps.filter) {
this.fetchTodos();
}
}
fetchTodos() {
this.setState({ loading: true });
fetch(`/api/todos?filter=${this.props.filter}`)
.then(res => res.json())
.then(todos => this.setState({ todos, loading: false }));
}
render() {
if (this.state.loading) return <div>Loading...</div>;
return (
<ul>
{this.state.todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
}componentDidMount handles the initial load. componentDidUpdate handles prop-driven re-fetches without duplicating the fetch logic. The prevProps check prevents an infinite loop.
Advanced: Debounced Search with Cleanup
class SearchResults extends React.Component {
state = { results: [] };
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.scheduleFetch();
}
}
scheduleFetch = () => {
// Arrow function keeps 'this' correct
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
fetch(`/api/search?q=${this.props.query}`)
.then(res => res.json())
.then(data => this.setState({ results: data.results }));
}, 300);
};
componentWillUnmount() {
clearTimeout(this.timeout); // cancel pending fetch if user navigates away
}
render() {
return (
<ul>
{this.state.results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
);
}
}Two things keep this correct. The debounce avoids a request on every keystroke. The cleanup in componentWillUnmount cancels any pending timeout if the component unmounts before the 300ms window closes, preventing a setState call on an unmounted component.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.