React Lifecycle Methods
Question 1: What are React lifecycle methods and how do they map to hooks?β
Answerβ
React lifecycle methods are special methods in class components that allow developers to hook into specific moments in a component's lifetime. These methods execute at different stages: when a component mounts (is created and inserted into the DOM), updates (re-renders due to prop or state changes), or unmounts (is removed from the DOM).
With the introduction of React Hooks in version 16.8, functional components gained the ability to use state and lifecycle features previously only available in class components. The primary hook for handling lifecycle events is useEffect, which can replicate the behavior of componentDidMount, componentDidUpdate, and componentWillUnmount through its dependency array and cleanup function mechanism.
Class Lifecycle Methods to Hooks Mapping:
| Class Method | Hook Equivalent | Purpose |
|---|---|---|
componentDidMount | useEffect(() => {}, []) | Run once after initial render |
componentDidUpdate | useEffect(() => {}) | Run after every render |
componentWillUnmount | useEffect(() => { return () => {} }, []) | Cleanup before unmounting |
shouldComponentUpdate | React.memo() / useMemo | Prevent unnecessary re-renders |
getDerivedStateFromProps | useState + useEffect | Sync state with props |
componentDidCatch | No direct equivalent (use Error Boundaries) | Error handling |
While class components use distinct methods for different lifecycle phases, hooks consolidate this logic into a single useEffect API, making code more concise and reducing the need to duplicate logic across multiple lifecycle methods.
<details> <summary><strong>π Deep Dive</strong></summary>
Class Component Lifecycle Architectureβ
React class components have three main lifecycle phases, each with specific methods:
1. Mounting Phase (Component Creation):
class MyComponent extends React.Component {
constructor(props) {
super(props);
// 1οΈβ£ First: Initialize state and bind methods
this.state = { count: 0 };
}
static getDerivedStateFromProps(props, state) {
// 2οΈβ£ Second: Sync state with props (rarely used)
// Must return state update object or null
return null;
}
render() {
// 3οΈβ£ Third: Return JSX
return <div>{this.state.count}</div>;
}
componentDidMount() {
// 4οΈβ£ Fourth: After DOM insertion
// Perfect for API calls, subscriptions, DOM manipulation
this.fetchData();
}
}
2. Updating Phase (Re-renders):
class MyComponent extends React.Component {
static getDerivedStateFromProps(props, state) {
// 1οΈβ£ First: Before every re-render
return null;
}
shouldComponentUpdate(nextProps, nextState) {
// 2οΈβ£ Second: Optimization point
// Return false to skip rendering
return nextState.count !== this.state.count;
}
render() {
// 3οΈβ£ Third: Create new React elements
return <div>{this.state.count}</div>;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 4οΈβ£ Fourth: Capture DOM info before changes
// Return value passed to componentDidUpdate
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 5οΈβ£ Fifth: After DOM updates
// Perfect for comparing prev/current state
if (prevState.count !== this.state.count) {
this.trackAnalytics();
}
}
}
3. Unmounting Phase (Component Removal):
class MyComponent extends React.Component {
componentWillUnmount() {
// Clean up subscriptions, timers, listeners
this.subscription.unsubscribe();
clearInterval(this.timer);
}
}
React Fiber and Lifecycle Executionβ
React Fiber (React 16+) introduced a two-phase rendering architecture:
Render Phase (Can be interrupted/paused):
constructorgetDerivedStateFromPropsshouldComponentUpdaterender
These methods must be pure (no side effects) because React may call them multiple times or discard the work.
Commit Phase (Synchronous, cannot be interrupted):
getSnapshotBeforeUpdatecomponentDidMountcomponentDidUpdatecomponentWillUnmount
Side effects (API calls, subscriptions) must only occur in commit phase methods.
Hooks Architecture: useEffect Deep Diveβ
useEffect consolidates all lifecycle behavior through two mechanisms:
1. Dependency Array Controls When Effect Runs:
// β BAD: Runs after EVERY render (performance issue)
useEffect(() => {
fetchData();
});
// β
GOOD: Runs only on mount
useEffect(() => {
fetchData();
}, []);
// β
GOOD: Runs when userId changes
useEffect(() => {
fetchData(userId);
}, [userId]);
// β
GOOD: Runs when either changes
useEffect(() => {
fetchData(userId, filter);
}, [userId, filter]);
2. Cleanup Function Handles Unmounting:
useEffect(() => {
const subscription = subscribeToData();
const timer = setInterval(() => {}, 1000);
// Cleanup runs:
// 1. Before effect re-runs (if dependencies changed)
// 2. When component unmounts
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, [dependencies]);
Complete Mapping Examplesβ
componentDidMount + componentWillUnmount:
// β Class Component (11 lines)
class DataFetcher extends React.Component {
componentDidMount() {
this.subscription = api.subscribe(this.props.userId, (data) => {
this.setState({ data });
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
}
// β
Functional Component (6 lines)
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
const subscription = api.subscribe(userId, setData);
return () => subscription.unsubscribe();
}, [userId]);
}
componentDidUpdate with Previous Value Comparison:
// β Class Component
class SearchComponent extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.query !== this.props.query) {
this.fetchResults(this.props.query);
}
}
}
// β
Functional Component
function SearchComponent({ query }) {
useEffect(() => {
fetchResults(query);
}, [query]); // Automatically runs only when query changes
}
shouldComponentUpdate β React.memo:
// β Class Component
class ExpensiveComponent extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.data !== this.props.data;
}
render() {
return <div>{/* expensive rendering */}</div>;
}
}
// β
Functional Component
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* expensive rendering */}</div>;
}, (prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.data === nextProps.data;
});
getDerivedStateFromProps Modern Alternativeβ
// β OLD: Class component with getDerivedStateFromProps
class UserProfile extends React.Component {
state = { displayName: '' };
static getDerivedStateFromProps(props, state) {
// Runs on every render, can cause bugs
if (props.userName !== state.prevUserName) {
return {
displayName: props.userName.toUpperCase(),
prevUserName: props.userName
};
}
return null;
}
}
// β
NEW: Functional component with useMemo
function UserProfile({ userName }) {
// Computed on demand, properly memoized
const displayName = useMemo(
() => userName.toUpperCase(),
[userName]
);
return <div>{displayName}</div>;
}
// β
OR: If you need stateful derived data
function UserProfile({ userName }) {
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(userName.toUpperCase());
}, [userName]);
}
</details>
<details> <summary><strong>π Real-World Scenario</strong></summary>
Scenario: E-commerce Product Search with Critical Lifecycle Bugsβ
Context: An e-commerce platform with 2M daily active users experienced severe performance degradation and memory leaks in their product search feature. Users reported slow search responses (5-8 seconds instead of <500ms) and browser crashes after prolonged use.
Initial Buggy Implementation:
// β PROBLEMATIC: Multiple lifecycle anti-patterns
class ProductSearch extends React.Component {
constructor(props) {
super(props);
this.state = {
results: [],
isLoading: false
};
}
// π¨ BUG #1: Side effects in constructor
constructor(props) {
super(props);
this.state = { results: [] };
// β NEVER do side effects in constructor!
fetch(`/api/products?q=${props.initialQuery}`)
.then(res => res.json())
.then(data => this.setState({ results: data }));
}
// π¨ BUG #2: setState in render (infinite loop)
render() {
if (this.props.query && !this.state.isLoading) {
// β Causes infinite re-render loop!
this.setState({ isLoading: true });
}
return <div>{/* ... */}</div>;
}
// π¨ BUG #3: Missing cleanup in componentDidMount
componentDidMount() {
// β Event listener never removed
window.addEventListener('scroll', this.handleScroll);
// β Interval never cleared
this.pollInterval = setInterval(() => {
this.fetchNewProducts();
}, 5000);
}
// π¨ BUG #4: componentDidUpdate without guards
componentDidUpdate(prevProps) {
// β No comparison, runs on EVERY update
this.fetchProducts(this.props.query);
// β Even worse: can trigger infinite loop
this.setState({ timestamp: Date.now() });
}
// π¨ BUG #5: Missing componentWillUnmount
// No cleanup = memory leak!
}
Production Impact Metrics:
- Search latency: 5,800ms average (target: <500ms)
- Memory leaks: 15MB/minute growth (100+ event listeners accumulating)
- Browser crashes: 234 crashes/day (mobile devices running out of memory)
- Bounce rate increase: 34% users abandoned search after 3+ seconds
- Revenue impact: Estimated $12K/day loss from abandoned searches
Debugging Process:
Step 1: React DevTools Profiler
# Identified components re-rendering 40+ times per search
# ProductSearch component: 87ms average render time
# Cascading re-renders in child components
Step 2: Chrome Performance Monitor
// Memory leak detected:
// - Initial: 45MB heap
// - After 5 minutes: 320MB heap
// - After 10 minutes: 580MB heap (browser crash)
// Event listeners growing:
console.log(window.getEventListeners(window).scroll.length);
// After 1 minute: 12 listeners
// After 5 minutes: 60 listeners
Step 3: Network Tab Analysis
# Duplicate API calls detected:
# - Same query called 4 times in 200ms
# - 600+ requests in 2 minutes (should be ~20)
Fixed Implementation (Class Component):
// β
FIXED: Proper lifecycle management
class ProductSearch extends React.Component {
constructor(props) {
super(props);
this.state = {
results: [],
isLoading: false
};
// β
Only initialization, no side effects
this.abortController = null;
}
componentDidMount() {
// β
Side effects in componentDidMount
this.fetchProducts(this.props.query);
// β
Store references for cleanup
this.handleScroll = this.handleScroll.bind(this);
window.addEventListener('scroll', this.handleScroll);
this.pollInterval = setInterval(() => {
this.fetchNewProducts();
}, 5000);
}
componentDidUpdate(prevProps) {
// β
Guard against unnecessary calls
if (prevProps.query !== this.props.query) {
this.fetchProducts(this.props.query);
}
}
componentWillUnmount() {
// β
Clean up ALL subscriptions and listeners
window.removeEventListener('scroll', this.handleScroll);
clearInterval(this.pollInterval);
// β
Cancel in-flight requests
if (this.abortController) {
this.abortController.abort();
}
}
fetchProducts(query) {
// β
Cancel previous request
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
this.setState({ isLoading: true });
fetch(`/api/products?q=${query}`, {
signal: this.abortController.signal
})
.then(res => res.json())
.then(data => {
this.setState({ results: data, isLoading: false });
})
.catch(err => {
if (err.name !== 'AbortError') {
this.setState({ isLoading: false });
}
});
}
render() {
// β
Pure render, no side effects
return (
<div>
{this.state.isLoading && <Spinner />}
{this.state.results.map(product => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
}
}
Migrated to Hooks (Best Solution):
// β
BEST: Functional component with hooks
function ProductSearch({ query }) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// β
Fetch products when query changes
useEffect(() => {
const abortController = new AbortController();
setIsLoading(true);
fetch(`/api/products?q=${query}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => {
setResults(data);
setIsLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setIsLoading(false);
}
});
// β
Cleanup runs automatically
return () => abortController.abort();
}, [query]); // Only re-run when query changes
// β
Separate effect for scroll listener
useEffect(() => {
const handleScroll = () => {
// Handle infinite scroll
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []); // Only mount/unmount
// β
Separate effect for polling
useEffect(() => {
const interval = setInterval(() => {
fetchNewProducts();
}, 5000);
return () => clearInterval(interval);
}, []); // Only mount/unmount
return (
<div>
{isLoading && <Spinner />}
{results.map(product => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
}
Post-Fix Metrics:
- Search latency: 420ms average (93% improvement)
- Memory usage: Stable at 48-52MB (no leaks)
- Browser crashes: 0 crashes/day
- API calls: Reduced from 600+ to 18 per session (97% reduction)
- Bounce rate: Dropped to 8% (74% improvement)
- Revenue recovery: $11K/day recovered
Key Lessons:
- Never put side effects in constructor/render - Use componentDidMount or useEffect
- Always clean up - Every subscription/listener needs cleanup
- Guard componentDidUpdate - Compare prev props/state to avoid infinite loops
- Cancel in-flight requests - Use AbortController to prevent state updates on unmounted components
- Separate concerns in hooks - Use multiple useEffect hooks instead of one giant effect
</details>
<details> <summary><strong>βοΈ Trade-offs</strong></summary>
Class Components vs Functional Components with Hooksβ
Class Components:
Advantages:
- Explicit lifecycle control - Clear separation between mount, update, unmount phases
- Error boundaries -
componentDidCatchandgetDerivedStateFromErroronly work in class components - Familiar to senior developers - Well-established patterns from pre-Hooks era
- Single update method -
componentDidUpdatehandles all prop/state changes in one place - Instance methods - Easy to share methods between lifecycle and event handlers
Disadvantages:
- Verbose boilerplate - More code for same functionality
- Logic duplication - Same logic split across componentDidMount and componentDidUpdate
thisbinding complexity - Need to bind methods or use arrow functions- Harder to extract reusable logic - No equivalent to custom hooks
- Bundle size - Classes don't minify as well as functions
Performance Comparison:
// Class Component: ~180 lines for complex feature
class UserDashboard extends React.Component {
componentDidMount() {
this.fetchUser();
this.subscribeToNotifications();
this.trackPageView();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
this.unsubscribeFromNotifications();
}
// + 15 more methods
}
// Functional Component: ~90 lines for same feature
function UserDashboard({ userId }) {
useEffect(() => {
fetchUser();
}, [userId]);
useEffect(() => {
const unsubscribe = subscribeToNotifications();
return unsubscribe;
}, []);
useEffect(() => {
trackPageView();
}, []);
// Custom hooks extract reusable logic
const userData = useUserData(userId);
const notifications = useNotifications();
}
Functional Components with Hooks:
Advantages:
- Less boilerplate - 40-60% less code for same functionality
- Reusable logic - Custom hooks extract and share stateful logic
- No
thisconfusion - Closures instead of instance binding - Easier testing - Pure functions, no class instance mocking
- Better code splitting - Tree-shaking works better with functions
- Concurrent features - Full support for Concurrent React features
Disadvantages:
- Learning curve - Closures, stale closures, useEffect dependencies can be confusing
- No error boundaries - Must wrap with class component for error handling
- Different mental model - Think in "effects" instead of lifecycle methods
- Dependency array footguns - Easy to miss dependencies or create infinite loops
- Stale closure bugs - Can accidentally capture old values
When to Use Each:
| Scenario | Recommendation | Reason |
|---|---|---|
| New projects | Functional + Hooks | Modern, less code, better tooling |
| Error boundaries | Class components | Only option currently |
| Large existing codebase | Mixed (migrate gradually) | Avoid big-bang rewrites |
| Simple presentational | Functional | No lifecycle needed anyway |
| Complex state logic | Functional + useReducer | More predictable than setState |
| Reusable logic | Custom hooks | Can't extract from classes |
| Performance-critical | Functional + React.memo | Better optimization options |
Lifecycle Complexity Trade-offsβ
Multiple useEffect vs Single componentDidUpdate:
// β ANTI-PATTERN: One giant useEffect
function Dashboard({ userId, filter, sortBy }) {
useEffect(() => {
// Hard to understand dependencies
fetchUserData(userId);
fetchFilteredResults(filter);
updateAnalytics(sortBy);
const subscription = subscribeToUpdates(userId, filter);
return () => subscription.unsubscribe();
}, [userId, filter, sortBy]); // All run when any changes
}
// β
BEST PRACTICE: Separate concerns
function Dashboard({ userId, filter, sortBy }) {
// Only runs when userId changes
useEffect(() => {
fetchUserData(userId);
}, [userId]);
// Only runs when filter changes
useEffect(() => {
fetchFilteredResults(filter);
}, [filter]);
// Only runs when sortBy changes
useEffect(() => {
updateAnalytics(sortBy);
}, [sortBy]);
// Subscription with specific dependencies
useEffect(() => {
const subscription = subscribeToUpdates(userId, filter);
return () => subscription.unsubscribe();
}, [userId, filter]);
}
Trade-off:
- Single effect: Fewer effects to manage, but over-executes
- Multiple effects: More granular control, but more code
Performance Impact:
- Single effect: ~150ms wasted work per update (running all logic)
- Multiple effects: ~40ms (only necessary logic runs)
Optimization Trade-offsβ
shouldComponentUpdate vs React.memo:
// Class Component: Fine-grained control
class ProductCard extends React.Component {
shouldComponentUpdate(nextProps) {
// Custom comparison logic
return (
nextProps.product.id !== this.props.product.id ||
nextProps.product.price !== this.props.product.price
// Ignore other props (like callbacks)
);
}
}
// Functional: Less control, but cleaner
const ProductCard = React.memo(
({ product, onSelect }) => {
return <div>{product.name}</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return (
prevProps.product.id === nextProps.product.id &&
prevProps.product.price === nextProps.product.price
);
}
);
Decision Matrix:
| Need | Use This | Avoid This |
|---|---|---|
| Prevent re-renders for expensive components | React.memo with comparison | Wrapping every component |
| Skip renders based on state | useMemo for computed values | shouldComponentUpdate in hooks |
| Memoize callbacks | useCallback | Creating new functions every render |
| Optimize child re-renders | React.memo on children | Premature optimization |
</details>
<details> <summary><strong>π¬ Explain to Junior</strong></summary>
The Life of a Plant Analogyβ
Think of a React component like a plant growing in your garden:
1. Mounting (Planting the Seed):
π± Seed planted β πΏ Sprout emerges β π³ Full tree
constructor render componentDidMount
When you plant a seed (create a component), it goes through stages:
- Constructor: The seed has its DNA (initial state)
- Render: The sprout emerges from soil (JSX appears in virtual DOM)
- componentDidMount: The plant is fully visible in your garden (component in real DOM)
// When you write <ProductCard />, React "plants" it:
function ProductCard() {
// 1οΈβ£ First: Component function runs (like seed sprouting)
const [count, setCount] = useState(0);
// 3οΈβ£ Third: After visible in browser (like roots established)
useEffect(() => {
console.log('Component is now in the DOM!');
// Perfect time to:
// - Fetch data from API (like plant absorbing nutrients)
// - Start subscriptions (like plant connecting to water source)
// - Set up timers (like plant's internal clock)
}, []); // Empty array = only run once when planted
// 2οΈβ£ Second: Return JSX (like plant becoming visible)
return <div>{count}</div>;
}
2. Updating (Plant Growing & Changing):
βοΈ Sunlight changes β π³ Leaves adjust β π New growth
Props/state change render componentDidUpdate
When conditions change (rain, sunlight, seasons), the plant adapts:
- Props/state change: Environment changes (like rain starting)
- Render: Plant adjusts (like leaves turning toward sun)
- componentDidUpdate: Plant finished adjusting (like new leaves fully grown)
function ProductCard({ productId }) {
const [data, setData] = useState(null);
// Runs when productId changes (like seasons changing)
useEffect(() => {
console.log('Product ID changed! Fetching new data...');
fetchProductData(productId).then(setData);
}, [productId]); // Runs whenever productId changes
return <div>{data?.name}</div>;
}
// When parent changes productId from 1 to 2:
// 1. React re-renders component (plant adjusts)
// 2. useEffect notices productId changed (season changed)
// 3. Effect runs, fetches new data (plant grows new leaves)
3. Unmounting (Removing the Plant):
βοΈ Gardener pulls plant β ποΈ Removed from garden
Component removed componentWillUnmount
When you remove a plant from your garden, you need to clean up:
- Remove stakes (cancel subscriptions)
- Turn off irrigation (clear timers)
- Clean up roots (remove event listeners)
function ProductCard() {
useEffect(() => {
// Plant the irrigation system
const interval = setInterval(() => {
console.log('Auto-watering...');
}, 1000);
// CLEANUP: When plant removed, disconnect irrigation!
return () => {
clearInterval(interval);
console.log('Irrigation disconnected');
};
}, []);
return <div>Product</div>;
}
// β WITHOUT cleanup:
// - Timers keep running (wasting water on empty soil)
// - Memory leaks (10 timers running for removed components)
// - Browser slows down (too many background tasks)
// β
WITH cleanup:
// - Timer stops when component removed
// - No memory leaks
// - Clean and efficient
Class Components vs Hooks (Simple Explanation)β
Class Components = Traditional Gardening:
- You have separate tools for each task
- Planting tool (componentDidMount)
- Watering schedule (componentDidUpdate)
- Removal tool (componentWillUnmount)
- More organized but more tools to carry
Hooks = Modern All-in-One Tool:
- One multitool (useEffect) does everything
- Plant, water, and remove with same tool
- Less to carry, but need to learn how to use it
// CLASS COMPONENT (Old way - separate tools)
class GardenPlant extends React.Component {
// Tool #1: Planting
componentDidMount() {
this.startWatering();
}
// Tool #2: Checking if needs care
componentDidUpdate(prevProps) {
if (prevProps.season !== this.props.season) {
this.adjustWatering();
}
}
// Tool #3: Removal
componentWillUnmount() {
this.stopWatering();
}
}
// FUNCTIONAL COMPONENT (New way - multitool)
function GardenPlant({ season }) {
useEffect(() => {
// Plant and start watering
startWatering();
// When removed, stop watering
return () => stopWatering();
}, []); // Only when planted/removed
useEffect(() => {
// Adjust when season changes
adjustWatering();
}, [season]); // When season changes
}
Interview Answer Templateβ
Question: "Explain React lifecycle methods and their hooks equivalents"
Template Answer:
"React lifecycle methods are special functions in class components that run at specific times during a component's existence. There are three main phases:
Mounting is when the component first appears. In class components, we use componentDidMount() to run code after the component is in the DOM, like fetching data. In hooks, we use useEffect with an empty dependency array: useEffect(() => { /* code */ }, []).
Updating happens when props or state change. Class components use componentDidUpdate(prevProps, prevState) to respond to changes. In hooks, we use useEffect with dependencies: useEffect(() => { /* code */ }, [dependency]).
Unmounting is cleanup before the component is removed. Class components use componentWillUnmount(), while hooks return a cleanup function from useEffect: useEffect(() => { return () => { /* cleanup */ } }, []).
The key difference is that class components have separate methods for each phase, while hooks consolidate everything into useEffect. This makes hooks code more concise and reduces duplication, but you need to understand dependency arrays to control when effects run.
For example, if you have a subscription that needs cleanup:
// Class
componentDidMount() {
this.subscription = subscribe();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
// Hooks (same logic, fewer lines)
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
Modern React development favors hooks because they're more concise, easier to test, and allow extracting reusable logic into custom hooks."
Question 2: What is the component lifecycle flow in React? (mounting, updating, unmounting)β
Answerβ
The React component lifecycle represents the sequence of phases a component goes through from creation to removal. Understanding this flow is crucial for managing side effects, optimizing performance, and preventing memory leaks.
Three Main Phases:
1. Mounting Phase (Birth): The component is being created and inserted into the DOM for the first time. This happens when you render a component initially or when a conditional component becomes visible.
Order of execution:
- Constructor / useState initialization
- getDerivedStateFromProps (class only)
- render()
- React updates DOM
- componentDidMount / useEffect with
[]
2. Updating Phase (Life): The component re-renders due to changes in props, state, or parent re-renders. This phase can happen many times during a component's lifetime.
Order of execution:
- getDerivedStateFromProps (class only)
- shouldComponentUpdate (class only)
- render()
- getSnapshotBeforeUpdate (class only)
- React updates DOM
- componentDidUpdate / useEffect with dependencies
3. Unmounting Phase (Death): The component is being removed from the DOM, either because a parent unmounted, a conditional render became false, or the component's route changed.
Order of execution:
- componentWillUnmount / useEffect cleanup function
Visual Flow:
Component Created β Mounted β Updated (0-N times) β Unmounted
β β β
Mount Update Cleanup
Phase Phase Phase
π Deep Diveβ
Complete Lifecycle Diagramβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MOUNTING PHASE β
β (Component being inserted into DOM for first time) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CLASS COMPONENT: FUNCTIONAL COMPONENT:
ββββββββββββββββββββ ββββββββββββββββββββ
β constructor β β Function call β
β (initialize β β useState init β
β state) β ββββββββββ¬ββββββββββ
ββββββββββ¬ββββββββββ β
β β
βΌ β
ββββββββββββββββββββ β
βgetDerivedState β ββββββββββΌββββββββββ
β FromProps β β render β
β (rare, avoid) β β (return JSX) β
ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ
β β
βΌ β
ββββββββββββββββββββ β
β render β β
β (return JSX) β β
ββββββββββ¬ββββββββββ β
β β
ββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββ
β React updates β
β real DOM β
ββββββββββ¬ββββββββββ
β
βΌ
ββββββββββββββββββββ ββββββββββββββββββββ
βcomponentDidMount β β useEffect with β
β (side effects) β β [] dependency β
ββββββββββββββββββββ ββββββββββββββββββββ
β β
ββββββββββββββββββββββββββββββββββββ
βΌ
Component is live!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UPDATING PHASE β
β (Props/state changed, parent re-rendered, or β
β forceUpdate called) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Trigger: setState(), new props, parent re-render, forceUpdate()
β
βΌ
ββββββββββββββββββββ ββββββββββββββββββββ
βgetDerivedState β β Function call β
β FromProps β β (re-execution) β
ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ
β β
βΌ β
ββββββββββββββββββββ β
βshouldComponent β β
β Update β β
β (optimization) β β
ββββββββββ¬ββββββββββ β
β β
β return false? ββββ> Skip β
β return true? ββ β
βΌ β β
ββββββββββββββββββββ β ββββββββββΌββββββββββ
β render β β β render β
ββββββββββ¬ββββββββββ β β (return JSX) β
β β ββββββββββ¬ββββββββββ
βΌ β β
ββββββββββββββββββββ β β
βgetSnapshotBefore β β β
β Update β β β
β(capture DOM info)β β β
ββββββββββ¬ββββββββββ β β
β β β
ββββββββββββ¬ββββ΄βββββββββββββββββββ
β
βΌ
ββββββββββββββββββββ
β React updates β
β real DOM β
ββββββββββ¬ββββββββββ
β
βΌ
ββββββββββββββββββββ ββββββββββββββββββββ
βcomponentDidUpdateβ βuseEffect cleanup β
β (prev props/ β β runs first, β
β state available)β β then effect runs β
ββββββββββββββββββββ ββββββββββββββββββββ
β β
ββββββββββββββββββββββββββββββββββββ
βΌ
Component updated!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UNMOUNTING PHASE β
β (Component being removed from DOM) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Trigger: Parent unmounts, conditional false, route change
β
βΌ
ββββββββββββββββββββ ββββββββββββββββββββ
βcomponentWill β β useEffect β
β Unmount β β cleanup β
β (cleanup: β β (return () => β
β timers, β β { cleanup }) β
β listeners, β β β
β subscriptions) β β β
ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ
β β
ββββββββββββββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββ
β React removes β
β from real DOM β
ββββββββββββββββββββ
βΌ
Component destroyed!
Detailed Phase Analysis with Code Examplesβ
1. Mounting Phase Deep Dive:
// ============================================
// CLASS COMPONENT MOUNTING
// ============================================
class UserProfile extends React.Component {
// 1οΈβ£ FIRST: Constructor runs
constructor(props) {
super(props);
console.log('1. Constructor - Initialize state');
this.state = {
user: null,
loading: true
};
// β
DO: Initialize state, bind methods
this.handleClick = this.handleClick.bind(this);
// β DON'T: Side effects, API calls, setState
// fetch('/api/user'); // NEVER!
}
// 2οΈβ£ SECOND: getDerivedStateFromProps (rare)
static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps');
// Only use for deriving state from props
// Most cases should use componentDidUpdate instead
if (props.userId !== state.prevUserId) {
return {
loading: true,
prevUserId: props.userId
};
}
return null;
}
// 3οΈβ£ THIRD: Render
render() {
console.log('3. Render - Create React elements');
// β
MUST be pure - no side effects!
// β DON'T: setState, API calls, subscriptions
return (
<div>
{this.state.loading ? 'Loading...' : this.state.user.name}
</div>
);
}
// 4οΈβ£ FOURTH: React updates DOM (internal)
// React takes JSX from render() and updates real DOM
// 5οΈβ£ FIFTH: componentDidMount
componentDidMount() {
console.log('5. componentDidMount - Component in DOM!');
// β
Perfect time for:
// - API calls
fetch(`/api/users/${this.props.userId}`)
.then(res => res.json())
.then(user => this.setState({ user, loading: false }));
// - Subscriptions
this.subscription = EventEmitter.subscribe('userUpdate', this.handleUpdate);
// - DOM measurements
const height = this.divRef.offsetHeight;
// - Third-party integrations
this.chart = new Chart(this.canvasRef, { /* config */ });
}
}
// ============================================
// FUNCTIONAL COMPONENT MOUNTING
// ============================================
function UserProfile({ userId }) {
// 1οΈβ£ FIRST: Function executes, hooks run
console.log('1. Function call - Component rendering');
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 2οΈβ£ SECOND: Render (return JSX)
console.log('2. Render - Create React elements');
// 3οΈβ£ THIRD: React updates DOM (internal)
// 4οΈβ£ FOURTH: useEffect with [] runs (equivalent to componentDidMount)
useEffect(() => {
console.log('4. useEffect (mount) - Component in DOM!');
// β
Same side effects as componentDidMount
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(user => {
setUser(user);
setLoading(false);
});
const subscription = EventEmitter.subscribe('userUpdate', handleUpdate);
// β
Return cleanup for unmount phase
return () => {
subscription.unsubscribe();
};
}, []); // Empty array = only run on mount
return (
<div>
{loading ? 'Loading...' : user.name}
</div>
);
}
2. Updating Phase Deep Dive:
// ============================================
// CLASS COMPONENT UPDATING
// ============================================
class ProductList extends React.Component {
state = {
products: [],
filters: { category: 'all' }
};
// 1οΈβ£ FIRST: getDerivedStateFromProps (every update)
static getDerivedStateFromProps(props, state) {
console.log('1. getDerivedStateFromProps (update)');
// Runs on EVERY update, be careful!
return null;
}
// 2οΈβ£ SECOND: shouldComponentUpdate (optimization gate)
shouldComponentUpdate(nextProps, nextState) {
console.log('2. shouldComponentUpdate');
// Return false to skip re-render
if (
nextProps.category === this.props.category &&
nextState.products === this.state.products
) {
console.log(' β Skipping update (no changes)');
return false;
}
console.log(' β Proceeding with update');
return true;
}
// 3οΈβ£ THIRD: render
render() {
console.log('3. Render (update)');
return (
<div ref={this.listRef}>
{this.state.products.map(p => (
<ProductCard key={p.id} {...p} />
))}
</div>
);
}
// 4οΈβ£ FOURTH: getSnapshotBeforeUpdate (capture pre-update DOM)
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('4. getSnapshotBeforeUpdate');
// Capture scroll position before DOM updates
if (prevState.products.length < this.state.products.length) {
return this.listRef.scrollHeight;
}
return null;
}
// 5οΈβ£ FIFTH: React updates DOM (internal)
// 6οΈβ£ SIXTH: componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('6. componentDidUpdate - DOM updated!');
// β
Compare previous vs current
if (prevProps.category !== this.props.category) {
console.log(' β Category changed, fetching new products');
this.fetchProducts(this.props.category);
}
// β
Use snapshot from getSnapshotBeforeUpdate
if (snapshot !== null) {
console.log(' β Adjusting scroll position');
this.listRef.scrollTop += (this.listRef.scrollHeight - snapshot);
}
// β DANGER: Infinite loop if no guards!
// this.setState({ ... }); // Must have condition!
}
fetchProducts(category) {
fetch(`/api/products?category=${category}`)
.then(res => res.json())
.then(products => this.setState({ products }));
}
}
// ============================================
// FUNCTIONAL COMPONENT UPDATING
// ============================================
function ProductList({ category }) {
const [products, setProducts] = useState([]);
const listRef = useRef(null);
// Effect runs when category changes
useEffect(() => {
console.log('useEffect (update) - category changed');
fetch(`/api/products?category=${category}`)
.then(res => res.json())
.then(products => setProducts(products));
}, [category]); // Dependency array = run when category changes
// Separate effect for scroll management
useEffect(() => {
console.log('useEffect (update) - products changed');
if (products.length > 0) {
// Adjust scroll position
listRef.current.scrollTop = 0;
}
}, [products]); // Run when products change
// Optimization with React.memo (like shouldComponentUpdate)
return (
<div ref={listRef}>
{products.map(p => (
<MemoizedProductCard key={p.id} {...p} />
))}
</div>
);
}
// Equivalent to shouldComponentUpdate
const MemoizedProductCard = React.memo(
ProductCard,
(prevProps, nextProps) => {
// Return true if equal (skip re-render)
return prevProps.id === nextProps.id &&
prevProps.price === nextProps.price;
}
);
3. Unmounting Phase Deep Dive:
// ============================================
// CLASS COMPONENT UNMOUNTING
// ============================================
class ChatRoom extends React.Component {
componentDidMount() {
// Set up resources
this.socket = io.connect('ws://chat.example.com');
this.socket.on('message', this.handleMessage);
this.timer = setInterval(() => {
this.checkOnlineStatus();
}, 5000);
window.addEventListener('beforeunload', this.handleUnload);
}
componentWillUnmount() {
console.log('componentWillUnmount - Cleaning up!');
// β
Clean up ALL resources
this.socket.disconnect();
this.socket.off('message', this.handleMessage);
clearInterval(this.timer);
window.removeEventListener('beforeunload', this.handleUnload);
// β
Cancel in-flight requests
if (this.abortController) {
this.abortController.abort();
}
// β DON'T: setState (component is dying!)
// this.setState({ ... }); // React will warn!
}
render() {
return <div>Chat Room</div>;
}
}
// ============================================
// FUNCTIONAL COMPONENT UNMOUNTING
// ============================================
function ChatRoom() {
useEffect(() => {
console.log('useEffect - Setting up resources');
const socket = io.connect('ws://chat.example.com');
socket.on('message', handleMessage);
const timer = setInterval(() => {
checkOnlineStatus();
}, 5000);
const handleUnload = () => { /* ... */ };
window.addEventListener('beforeunload', handleUnload);
// β
Cleanup function (runs on unmount)
return () => {
console.log('useEffect cleanup - Cleaning up!');
socket.disconnect();
socket.off('message', handleMessage);
clearInterval(timer);
window.removeEventListener('beforeunload', handleUnload);
};
}, []); // Empty array = only mount/unmount
return <div>Chat Room</div>;
}
Commit Phase vs Render Phaseβ
React Fiber architecture splits lifecycle into two phases:
Render Phase (Can be paused/restarted):
- Constructor
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- Must be pure (no side effects)
Commit Phase (Synchronous, cannot be interrupted):
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
- Can have side effects (API calls, subscriptions)
// β WRONG: Side effect in render phase
function BadComponent() {
// This runs during render phase - can run multiple times!
fetch('/api/data'); // NEVER!
return <div>Bad</div>;
}
// β
CORRECT: Side effect in commit phase
function GoodComponent() {
useEffect(() => {
// This runs during commit phase - guaranteed once
fetch('/api/data');
}, []);
return <div>Good</div>;
}
</details>
<details> <summary><strong>π Real-World Scenario</strong></summary>
Scenario: Social Media Feed with Critical Lifecycle Flow Issuesβ
Context: A social media platform with 5M users experienced cascading failures in their news feed component. Users reported infinite loading spinners, duplicate posts appearing, memory consumption spiking to 2GB+ after 10 minutes of scrolling, and the browser becoming unresponsive.
Production Metrics (Before Fix):
- Memory leaks: 180MB/minute growth rate
- Duplicate API calls: 47 calls for single feed load (should be 1)
- Render performance: 2,400ms for feed update (target: <100ms)
- Crash rate: 890 browser crashes/day
- User engagement: Down 42% (users abandoning app)
Initial Problematic Implementation:
// β CRITICAL BUGS: Multiple lifecycle anti-patterns
class NewsFeed extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
page: 1,
loading: false
};
// π¨ BUG #1: API call in constructor
// Runs BEFORE component is in DOM
// Can cause race conditions
fetch(`/api/feed?page=${this.state.page}`)
.then(res => res.json())
.then(posts => {
// May call setState before component mounted!
this.setState({ posts });
});
}
componentDidMount() {
// π¨ BUG #2: No cleanup for scroll listener
window.addEventListener('scroll', this.handleScroll);
// π¨ BUG #3: Redundant fetch (already called in constructor)
this.fetchPosts();
// π¨ BUG #4: WebSocket never closed
this.ws = new WebSocket('wss://api.example.com/live');
this.ws.onmessage = (event) => {
const newPost = JSON.parse(event.data);
this.setState(prevState => ({
posts: [newPost, ...prevState.posts]
}));
};
}
componentDidUpdate(prevProps, prevState) {
// π¨ BUG #5: No guards, runs on EVERY update
this.fetchPosts(); // Infinite loop!
// π¨ BUG #6: setState without condition
this.setState({ timestamp: Date.now() }); // Triggers another update!
// π¨ BUG #7: Expensive DOM operation every update
const element = document.getElementById('feed-container');
element.style.height = `${window.innerHeight}px`;
}
// π¨ BUG #8: Missing componentWillUnmount entirely!
// No cleanup = massive memory leaks
handleScroll = () => {
// π¨ BUG #9: Not debounced, fires 100+ times/second
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight;
if (bottom && !this.state.loading) {
this.setState({ page: this.state.page + 1 }, () => {
this.fetchPosts();
});
}
};
fetchPosts() {
// π¨ BUG #10: No abort mechanism
fetch(`/api/feed?page=${this.state.page}`)
.then(res => res.json())
.then(posts => {
// Can set state after unmount!
this.setState(prevState => ({
posts: [...prevState.posts, ...posts]
}));
});
}
render() {
return (
<div id="feed-container">
{this.state.posts.map(post => (
<Post key={post.id} {...post} />
))}
</div>
);
}
}
Debugging Process:
Step 1: React DevTools Profiler
# Flamegraph Analysis:
- NewsFeed component: 2,400ms render time
- componentDidUpdate called 87 times in 3 seconds
- Each update triggers fetchPosts (87 API calls!)
- Cascading re-renders in child Post components
Step 2: Chrome Performance Monitor
// Memory snapshot comparison:
// Initial: 65MB
// After 1 minute: 245MB (+180MB)
// After 5 minutes: 965MB (+900MB)
// After 10 minutes: 1.8GB (browser crash)
// Event listeners analysis:
console.log(getEventListeners(window).scroll);
// Expected: 1 scroll listener
// Actual: 143 scroll listeners (each feed instance adding more!)
Step 3: Network Tab Analysis
# Waterfall view:
- Same API endpoint called 47 times in 2 seconds
- Requests not cancelling when component updates
- 16 simultaneous WebSocket connections (should be 1)
Step 4: Console Warnings
// Warning: Can't perform a React state update on an unmounted component
// (fetchPosts completing after unmount - 234 occurrences)
// Warning: Maximum update depth exceeded
// (componentDidUpdate setState loop - infinite)
Fixed Implementation (Class Component):
// β
FIXED: Proper lifecycle management
class NewsFeed extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
page: 1,
loading: false,
hasMore: true
};
// β
Only initialization, no side effects
this.abortController = null;
this.ws = null;
this.handleScroll = this.debounce(this.handleScroll.bind(this), 200);
}
componentDidMount() {
// β
Single fetch on mount
this.fetchPosts();
// β
Store reference for cleanup
window.addEventListener('scroll', this.handleScroll);
// β
Initialize WebSocket
this.initializeWebSocket();
}
componentDidUpdate(prevProps, prevState) {
// β
Guard: Only fetch if page changed
if (prevState.page !== this.state.page) {
this.fetchPosts();
}
// β
Guard: Only update if userId changed
if (prevProps.userId !== this.props.userId) {
this.resetFeed();
}
// β
No setState without guards!
}
componentWillUnmount() {
// β
Clean up scroll listener
window.removeEventListener('scroll', this.handleScroll);
// β
Cancel in-flight requests
if (this.abortController) {
this.abortController.abort();
}
// β
Close WebSocket
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
initializeWebSocket() {
this.ws = new WebSocket('wss://api.example.com/live');
this.ws.onmessage = (event) => {
const newPost = JSON.parse(event.data);
// β
Check if still mounted
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.setState(prevState => ({
posts: [newPost, ...prevState.posts]
}));
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
// Auto-reconnect logic here
};
}
fetchPosts() {
// β
Cancel previous request
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
this.setState({ loading: true });
fetch(`/api/feed?page=${this.state.page}`, {
signal: this.abortController.signal
})
.then(res => res.json())
.then(data => {
this.setState(prevState => ({
posts: [...prevState.posts, ...data.posts],
loading: false,
hasMore: data.hasMore
}));
})
.catch(err => {
if (err.name !== 'AbortError') {
this.setState({ loading: false });
}
});
}
handleScroll() {
if (this.state.loading || !this.state.hasMore) return;
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 100;
if (bottom) {
this.setState(prevState => ({
page: prevState.page + 1
}));
}
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
resetFeed() {
this.setState({
posts: [],
page: 1,
hasMore: true
});
}
render() {
return (
<div>
{this.state.posts.map(post => (
<Post key={post.id} {...post} />
))}
{this.state.loading && <Spinner />}
</div>
);
}
}
Modern Solution with Hooks:
// β
BEST: Functional component with proper lifecycle hooks
function NewsFeed({ userId }) {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
// β
Fetch posts when page changes
useEffect(() => {
const abortController = new AbortController();
setLoading(true);
fetch(`/api/feed?page=${page}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => {
setPosts(prev => [...prev, ...data.posts]);
setHasMore(data.hasMore);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setLoading(false);
}
});
// β
Cleanup cancels request
return () => abortController.abort();
}, [page]); // Only when page changes
// β
Reset feed when userId changes
useEffect(() => {
setPosts([]);
setPage(1);
setHasMore(true);
}, [userId]);
// β
Scroll listener with cleanup
useEffect(() => {
const handleScroll = debounce(() => {
if (loading || !hasMore) return;
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 100;
if (bottom) {
setPage(prev => prev + 1);
}
}, 200);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [loading, hasMore]); // Re-create when these change
// β
WebSocket with cleanup
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/live');
ws.onmessage = (event) => {
const newPost = JSON.parse(event.data);
setPosts(prev => [newPost, ...prev]);
};
return () => ws.close();
}, []); // Only mount/unmount
return (
<div>
{posts.map(post => (
<Post key={post.id} {...post} />
))}
{loading && <Spinner />}
</div>
);
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
Post-Fix Metrics:
- Memory usage: Stable at 58-65MB (96% reduction)
- API calls: 1 call per page load (97% reduction)
- Render performance: 68ms average (97% improvement)
- Crash rate: 0 crashes/day (100% elimination)
- User engagement: Recovered to +8% above baseline
- Scroll listener leaks: 0 (was accumulating 14/minute)
Root Causes Identified:
- Side effects in constructor - Caused race conditions and duplicate calls
- Missing componentWillUnmount - Event listeners and WebSockets leaked
- Unguarded componentDidUpdate - Created infinite update loops
- No request cancellation - setState on unmounted components
- Non-debounced scroll - 100+ handleScroll calls per second
</details>
<details> <summary><strong>βοΈ Trade-offs</strong></summary>
Lifecycle Complexity vs Controlβ
Detailed Lifecycle Methods (Class Components):
Advantages:
- Explicit control - Separate methods for mount vs update
- Optimization points -
shouldComponentUpdateandgetSnapshotBeforeUpdate - Clear phase separation - Render phase vs commit phase
- Easier debugging - Stack traces show exact lifecycle method
- Error boundaries - Only available in class components
Disadvantages:
- Code duplication - Same logic in mount and update
- Verbose - 3+ methods for single feature
- Easy to misuse - Common to put side effects in wrong phase
- Hard to extract - Can't share lifecycle logic across components
- Mental model - Need to think about three separate phases
Consolidated useEffect (Functional Components):
Advantages:
- Less code - Single useEffect for mount + update + cleanup
- Co-located logic - Setup and cleanup in same place
- Reusable - Extract into custom hooks
- Dependency tracking - Automatic re-run when dependencies change
- Better code splitting - Easier to lazy-load
Disadvantages:
- Stale closures - Can capture old values
- Dependency array confusion - Easy to miss dependencies
- No error boundaries - Must use class component wrapper
- Over-execution - Without proper dependencies, runs too often
- Debugging complexity - Multiple effects can be hard to trace
Decision Matrix: When to Use Which Patternβ
| Scenario | Use This | Reason |
|---|---|---|
| New feature development | Functional + hooks | Modern, less code, better tooling |
| Error boundary | Class component | Only option for componentDidCatch |
| Legacy codebase (all classes) | Class component | Consistency, avoid mixed patterns |
| Need getSnapshotBeforeUpdate | Class component | No hooks equivalent |
| Reusable logic across components | Custom hooks | Can't extract from classes |
| Performance-critical list | Functional + React.memo | Better optimization options |
| Third-party library integration | Either (prefer hooks) | Hooks have cleaner cleanup |
| Testing | Functional + hooks | Easier to test, no mocking this |
Performance Comparisonβ
Memory Usage:
// Class Component: ~8KB per instance
class MyComponent extends React.Component {
// Instance properties: 8KB
// Prototype methods: shared
// Event handler bindings: 2KB per handler
}
// Functional Component: ~3KB per instance
function MyComponent() {
// Closures: 3KB
// No instance, no prototype
// Inline handlers: optimized by React
}
Bundle Size:
# Class components:
- Larger minified size (class syntax doesn't minify well)
- Example: 45KB for complex component
# Functional components:
- Smaller minified size (functions minify better)
- Example: 28KB for same component (38% smaller)
Execution Speed:
// Benchmark: 10,000 component mounts
// Class component: 240ms
// - Constructor overhead
// - Instance creation
// - Method binding
// Functional component: 180ms (25% faster)
// - Direct function call
// - No instance overhead
// - Hooks optimized by React
Lifecycle Granularity Trade-offsβ
Single Effect vs Multiple Effects:
// β ANTI-PATTERN: One giant effect (hard to debug)
useEffect(() => {
// Everything in one effect
fetchUser();
subscribeToNotifications();
trackAnalytics();
setupWebSocket();
return () => {
// Complex cleanup
unsubscribe();
closeWebSocket();
};
}, [userId, settings, preferences]); // Too many dependencies
// β
BEST PRACTICE: Separate concerns
useEffect(() => {
fetchUser();
}, [userId]); // Only when userId changes
useEffect(() => {
const unsubscribe = subscribeToNotifications();
return unsubscribe;
}, []); // Only mount/unmount
useEffect(() => {
trackAnalytics();
}, [userId, currentPage]); // When either changes
useEffect(() => {
const ws = setupWebSocket();
return () => ws.close();
}, [userId]); // When userId changes
Trade-off Analysis:
- Single effect: 4 fewer useEffect calls (minor perf), but over-executes
- Multiple effects: More granular control, better performance, easier debugging
- Recommended: Split by concern (data fetching, subscriptions, analytics)
Optimization Strategies Comparisonβ
Class Component Optimization:
class ProductList extends React.Component {
// Option 1: shouldComponentUpdate (manual)
shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.products !== this.props.products ||
nextState.filter !== this.state.filter
);
}
// Option 2: PureComponent (shallow comparison)
// class ProductList extends React.PureComponent {
}
// Pros: Fine-grained control, can optimize based on business logic
// Cons: Easy to get wrong, must manually compare all relevant props/state
Functional Component Optimization:
// Option 1: React.memo (component level)
const ProductList = React.memo(({ products, filter }) => {
// ...
}, (prevProps, nextProps) => {
return prevProps.products === nextProps.products &&
prevProps.filter === nextProps.filter;
});
// Option 2: useMemo (value level)
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === filter);
}, [products, filter]);
}
// Pros: More flexible, can memoize individual values
// Cons: Easy to over-memoize, dependency array pitfalls
</details>
<details> <summary><strong>π¬ Explain to Junior</strong></summary>
The Restaurant Service Analogyβ
Think of a React component like a restaurant serving a customer:
1. Mounting Phase (Customer Arrives):
πΆ Customer enters β πͺ Seated at table β π½οΈ Server takes order
Component created Rendered to DOM Side effects run
Example:
function Restaurant({ customerId }) {
const [order, setOrder] = useState(null);
// 1οΈβ£ Customer enters (component created)
console.log('Customer arriving...');
// 3οΈβ£ Server takes order (after seated)
useEffect(() => {
console.log('Welcome! Taking your order...');
// Fetch customer's favorite order
fetchFavoriteOrder(customerId).then(setOrder);
// Set up notification for when food is ready
const subscription = kitchenNotifications.subscribe(customerId);
// When customer leaves, clean up
return () => subscription.unsubscribe();
}, []); // Only when customer first arrives
// 2οΈβ£ Customer seated (component rendered)
return <div>Welcome, customer #{customerId}!</div>;
}
2. Updating Phase (Order Changes):
π£οΈ Customer changes order β π½οΈ Server updates kitchen β π New food arrives
Props/state change Re-render Effect runs
Example:
function Restaurant({ customerId, tableNumber }) {
const [order, setOrder] = useState(null);
// When customer moves to different table
useEffect(() => {
console.log(`Customer moved to table ${tableNumber}`);
updateTableAssignment(customerId, tableNumber);
}, [tableNumber]); // Only when table changes
// When customer ID changes (new customer!)
useEffect(() => {
console.log('New customer, fetching their order...');
fetchFavoriteOrder(customerId).then(setOrder);
}, [customerId]); // Only when customer changes
return (
<div>
Table {tableNumber}: {order?.name}
</div>
);
}
Why separate effects?
- Changing tables doesn't need new order fetch
- New customer needs everything reset
- Each effect runs only when relevant
3. Unmounting Phase (Customer Leaves):
π΅ Customer pays β π§Ή Clean table β β¨ Table ready for next customer
Component unmounts Cleanup runs Memory freed
Example:
function Restaurant({ customerId }) {
useEffect(() => {
// Customer arrives, start their service
const kitchenAlert = subscribeToKitchen(customerId);
const waiterAlert = assignWaiter(customerId);
const timer = setInterval(() => {
checkIfNeedsRefill(customerId);
}, 60000); // Check every minute
// CLEANUP: When customer leaves
return () => {
console.log('Customer leaving, cleaning up...');
kitchenAlert.unsubscribe(); // Stop kitchen notifications
waiterAlert.dismiss(); // Free up waiter
clearInterval(timer); // Stop checking for refills
cleanTable(); // Reset for next customer
};
}, []);
return <div>Serving customer...</div>;
}
// β WITHOUT cleanup:
// - 100 customers served = 100 timers still running!
// - 100 waiter alerts piling up
// - Memory leak, restaurant can't handle new customers
// β
WITH cleanup:
// - Each customer cleaned up when they leave
// - Resources freed for next customer
// - Efficient, no memory leaks
Common Lifecycle Mistakes (Simple Explanations)β
Mistake #1: Forgetting Cleanup
// β BAD: Like leaving dirty dishes on table forever
function BadRestaurant() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Checking kitchen...');
}, 1000);
// FORGOT TO CLEAN UP!
// Timer runs forever even after restaurant closes
}, []);
}
// β
GOOD: Clean up dirty dishes when customer leaves
function GoodRestaurant() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Checking kitchen...');
}, 1000);
return () => {
clearInterval(timer); // Stop timer when done!
};
}, []);
}
Mistake #2: Running Effects Too Often
// β BAD: Asking customer's name every second (annoying!)
function BadRestaurant({ customerName }) {
useEffect(() => {
console.log('What is your name?');
updateNameTag(customerName);
}); // No dependency array = runs EVERY render!
}
// β
GOOD: Only ask name when it actually changes
function GoodRestaurant({ customerName }) {
useEffect(() => {
console.log('Updating name tag...');
updateNameTag(customerName);
}, [customerName]); // Only when name changes
}
Mistake #3: Infinite Loops
// β BAD: Customer asks for water β Waiter brings water β Customer asks for water β infinite!
function BadRestaurant() {
const [waterRequests, setWaterRequests] = useState(0);
useEffect(() => {
// This causes infinite loop!
setWaterRequests(waterRequests + 1);
}, [waterRequests]); // Runs when waterRequests changes, which we change inside!
}
// β
GOOD: Only refill when glass is empty
function GoodRestaurant({ glassEmpty }) {
const [waterRefills, setWaterRefills] = useState(0);
useEffect(() => {
if (glassEmpty) {
setWaterRefills(prev => prev + 1);
}
}, [glassEmpty]); // Only when glass becomes empty
}
Interview Answer Templateβ
Question: "Explain the React component lifecycle flow"
Template Answer:
"React components go through three main lifecycle phases: mounting, updating, and unmounting.
Mounting is when the component first appears in the DOM. In class components, this involves the constructor initializing state, render creating JSX, and then componentDidMount running side effects like API calls. In functional components, the component function runs, returns JSX, and then useEffect with an empty dependency array runs, which is equivalent to componentDidMount.
Updating happens when props or state change, causing a re-render. In class components, render is called again, and componentDidUpdate runs after the DOM updates, where you can compare previous and current props/state. In functional components, the function re-executes, and useEffect with dependencies runs only when those dependencies change.
Unmounting is cleanup before the component is removed. Class components use componentWillUnmount to clean up subscriptions, timers, and event listeners. Functional components return a cleanup function from useEffect that runs when the component unmounts.
The key difference is that class components have separate methods for each phase (componentDidMount, componentDidUpdate, componentWillUnmount), while functional components consolidate everything into useEffect using dependency arrays to control when effects run.
For example, a subscription in class components requires two methods:
// Class
componentDidMount() {
this.subscription = subscribe();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
// Hooks (cleaner)
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
The most important rule is to always clean up side effects to prevent memory leaks, and to use dependency arrays in useEffect to control when effects re-run, avoiding infinite loops or unnecessary executions."
</details>
Source: lifecycle-01-methods.md