frontend / react / lifecycle.md

React Lifecycle

4 interview angles 10 min read source

React Lifecycle

React lifecycle methods are special methods that are called at different stages of a component’s existence, from creation to destruction.

Table of Contents


What is React Lifecycle?

React lifecycle refers to the series of methods that are called during the different phases of a component’s existence: mounting, updating, and unmounting.

Key Concepts:

  • Mounting: Component is being created and inserted into DOM
  • Updating: Component is re-rendering due to props/state changes
  • Unmounting: Component is being removed from DOM
  • Error Handling: Catching errors during rendering

Lifecycle Phases:

// Component lifecycle flow
class Component extends React.Component {
  // 1. Mounting Phase
  constructor() { /* Initialize state */ }
  static getDerivedStateFromProps() { /* Update state from props */ }
  render() { /* Return JSX */ }
  componentDidMount() { /* Side effects after mount */ }

  // 2. Updating Phase
  static getDerivedStateFromProps() { /* Update state from props */ }
  shouldComponentUpdate() { /* Control re-rendering */ }
  render() { /* Return JSX */ }
  getSnapshotBeforeUpdate() { /* Capture info before update */ }
  componentDidUpdate() { /* Side effects after update */ }

  // 3. Unmounting Phase
  componentWillUnmount() { /* Cleanup before unmount */ }

  // 4. Error Handling
  componentDidCatch() { /* Handle errors */ }
}

Class Component Lifecycle

1. Mounting Phase:

class UserProfile extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      loading: true
    };
    console.log('1. Constructor called');
  }

  static getDerivedStateFromProps(props, state) {
    console.log('2. getDerivedStateFromProps called');
    // Update state based on props
    if (props.userId && !state.user) {
      return { userId: props.userId };
    }
    return null;
  }

  componentDidMount() {
    console.log('4. componentDidMount called');
    // Fetch user data
    this.fetchUser();
  }

  fetchUser = async () => {
    try {
      const user = await fetch(`/api/users/${this.state.userId}`);
      this.setState({ user, loading: false });
    } catch (error) {
      console.error('Error fetching user:', error);
    }
  };

  render() {
    console.log('3. Render called');

    if (this.state.loading) {
      return <div>Loading...</div>;
    }

    return (
      <div>
        <h2>{this.state.user?.name}</h2>
        <p>{this.state.user?.email}</p>
      </div>
    );
  }
}

2. Updating Phase:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  static getDerivedStateFromProps(props, state) {
    console.log('1. getDerivedStateFromProps (update)');
    // Update state if props change
    if (props.initialCount !== state.count && props.initialCount !== undefined) {
      return { count: props.initialCount };
    }
    return null;
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('2. shouldComponentUpdate');
    // Only update if count changed
    return nextState.count !== this.state.count;
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('4. getSnapshotBeforeUpdate');
    // Capture scroll position before update
    return this.listRef.current?.scrollTop;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('6. componentDidUpdate');

    // Restore scroll position
    if (snapshot !== null && this.listRef.current) {
      this.listRef.current.scrollTop = snapshot;
    }

    // Log count changes
    if (prevState.count !== this.state.count) {
      console.log(`Count changed from ${prevState.count} to ${this.state.count}`);
    }
  }

  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  render() {
    console.log('3. Render (update)');
    return (
      <div>
        <h2>Count: {this.state.count}</h2>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

3. Unmounting Phase:

class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
    this.intervalId = null;
  }

  componentDidMount() {
    this.intervalId = setInterval(() => {
      this.setState(prevState => ({ seconds: prevState.seconds + 1 }));
    }, 1000);
  }

  componentWillUnmount() {
    console.log('Component will unmount - cleaning up');
    // Clean up interval to prevent memory leaks
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }

  render() {
    return <div>Seconds: {this.state.seconds}</div>;
  }
}

// Usage
function App() {
  const [showTimer, setShowTimer] = useState(true);

  return (
    <div>
      {showTimer && <Timer />}
      <button onClick={() => setShowTimer(!showTimer)}>
        {showTimer ? 'Hide Timer' : 'Show Timer'}
      </button>
    </div>
  );
}

4. Error Handling:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    console.log('getDerivedStateFromError called');
    // Update state to show error UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.log('componentDidCatch called');
    // Log error to service
    console.error('Error caught by boundary:', error, errorInfo);

    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Component that might throw
class RiskyComponent extends React.Component {
  componentDidMount() {
    if (this.props.shouldThrow) {
      throw new Error('Component error!');
    }
  }

  render() {
    return <div>Safe content</div>;
  }
}

Functional Component Lifecycle

1. useEffect Hook:

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Equivalent to componentDidMount
  useEffect(() => {
    console.log('Component mounted');
    fetchUser();
  }, []); // Empty dependency array = run only on mount

  // Equivalent to componentDidUpdate
  useEffect(() => {
    console.log('User ID changed:', userId);
    if (userId) {
      fetchUser();
    }
  }, [userId]); // Run when userId changes

  // Equivalent to componentWillUnmount
  useEffect(() => {
    return () => {
      console.log('Component will unmount');
      // Cleanup code here
    };
  }, []);

  const fetchUser = async () => {
    try {
      setLoading(true);
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('Error fetching user:', error);
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h2>{user?.name}</h2>
      <p>{user?.email}</p>
    </div>
  );
}

2. Multiple useEffect Hooks:

function ComplexComponent({ userId, theme }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  // Fetch user data
  useEffect(() => {
    if (userId) {
      fetchUser(userId);
    }
  }, [userId]);

  // Fetch user posts
  useEffect(() => {
    if (user) {
      fetchPosts(user.id);
    }
  }, [user]);

  // Listen to window resize
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);

    // Cleanup function
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  // Update document title
  useEffect(() => {
    document.title = user ? `${user.name}'s Profile` : 'User Profile';
  }, [user]);

  // Theme effect
  useEffect(() => {
    document.body.className = theme;
  }, [theme]);

  const fetchUser = async (id) => {
    const response = await fetch(`/api/users/${id}`);
    const userData = await response.json();
    setUser(userData);
  };

  const fetchPosts = async (userId) => {
    const response = await fetch(`/api/users/${userId}/posts`);
    const postsData = await response.json();
    setPosts(postsData);
  };

  return (
    <div>
      <h2>{user?.name}</h2>
      <p>Window size: {windowSize.width}x{windowSize.height}</p>
      <div>
        {posts.map(post => (
          <div key={post.id}>{post.title}</div>
        ))}
      </div>
    </div>
  );
}

Common Lifecycle Patterns

1. Data Fetching:

// Class component pattern
class DataFetcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: null, loading: true, error: null };
  }

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.url !== this.props.url) {
      this.fetchData();
    }
  }

  componentWillUnmount() {
    // Cancel any pending requests
    if (this.controller) {
      this.controller.abort();
    }
  }

  fetchData = async () => {
    try {
      this.setState({ loading: true, error: null });

      // Create abort controller for cleanup
      this.controller = new AbortController();

      const response = await fetch(this.props.url, {
        signal: this.controller.signal
      });

      if (!response.ok) {
        throw new Error('Network response was not ok');
      }

      const data = await response.json();
      this.setState({ data, loading: false });
    } catch (error) {
      if (error.name !== 'AbortError') {
        this.setState({ error: error.message, loading: false });
      }
    }
  };

  render() {
    if (this.state.loading) return <div>Loading...</div>;
    if (this.state.error) return <div>Error: {this.state.error}</div>;
    if (!this.state.data) return <div>No data</div>;

    return <div>{JSON.stringify(this.state.data)}</div>;
  }
}

// Functional component pattern
function DataFetcher({ url }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }

        const result = await response.json();

        if (isMounted) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!data) return <div>No data</div>;

  return <div>{JSON.stringify(data)}</div>;
}

2. Event Listeners:

// Class component
class WindowResizer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { windowSize: { width: window.innerWidth, height: window.innerHeight } };
  }

  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }

  handleResize = () => {
    this.setState({
      windowSize: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    });
  };

  render() {
    return (
      <div>
        Window size: {this.state.windowSize.width} x {this.state.windowSize.height}
      </div>
    );
  }
}

// Functional component
function WindowResizer() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      Window size: {windowSize.width} x {windowSize.height}
    </div>
  );
}

3. Timer Management:

// Class component
class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }

  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState(prevState => ({ seconds: prevState.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(prev => prev + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>Seconds: {seconds}</div>;
}

Best Practices

1. Use useEffect for Side Effects:

// Good: Use useEffect for side effects
function GoodComponent({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId);
  }, [userId]);

  return <div>{user?.name}</div>;
}

// Bad: Don't put side effects in render
function BadComponent({ userId }) {
  const [user, setUser] = useState(null);

  // This will cause infinite re-renders
  fetchUser(userId); // Don't do this in render

  return <div>{user?.name}</div>;
}

2. Clean Up Side Effects:

// Good: Clean up subscriptions and timers
function GoodComponent() {
  useEffect(() => {
    const subscription = someAPI.subscribe();

    return () => {
      subscription.unsubscribe();
    };
  }, []);
}

// Bad: Don't forget cleanup
function BadComponent() {
  useEffect(() => {
    someAPI.subscribe(); // Memory leak!
  }, []);
}

3. Use Dependency Arrays Correctly:

// Good: Include all dependencies
function GoodComponent({ userId, theme }) {
  useEffect(() => {
    fetchUser(userId);
  }, [userId]); // Include userId

  useEffect(() => {
    document.body.className = theme;
  }, [theme]); // Include theme
}

// Bad: Missing dependencies
function BadComponent({ userId }) {
  useEffect(() => {
    fetchUser(userId);
  }, []); // Missing userId dependency
}

4. Avoid Infinite Loops:

// Good: Stable dependencies
function GoodComponent({ user }) {
  const userString = JSON.stringify(user);

  useEffect(() => {
    console.log('User changed:', userString);
  }, [userString]);
}

// Bad: Unstable dependencies
function BadComponent({ user }) {
  useEffect(() => {
    console.log('User changed:', user);
  }, [user]); // user object changes on every render
}

Common Interview Questions

Q: What are React lifecycle methods?

  • Special methods called at different stages of a component’s existence: mounting, updating, and unmounting.

Q: What is the difference between componentDidMount and useEffect?

  • componentDidMount is a class component lifecycle method, useEffect is a hook for functional components.

Q: When do you use componentDidMount vs componentDidUpdate?

  • componentDidMount runs once after initial render, componentDidUpdate runs after every re-render.

Q: How do you handle cleanup in functional components?

  • Return a cleanup function from useEffect to handle unmounting and dependency changes.

Q: What is getDerivedStateFromProps used for?

  • To update state based on prop changes before rendering.

Q: How do you prevent unnecessary re-renders?

  • Use shouldComponentUpdate in class components or React.memo for functional components.

Q: What is componentWillUnmount used for?

  • Cleanup tasks like canceling subscriptions, timers, or network requests.

Q: How do you handle errors in React components?

  • Use error boundaries with componentDidCatch or getDerivedStateFromError.

Q: What’s the difference between useEffect with empty array and no array?

  • Empty array runs only on mount, no array runs after every render.

Q: How do you handle async operations in useEffect?

  • Use async functions inside useEffect and handle cleanup with AbortController.

Summary

  • React lifecycle consists of mounting, updating, and unmounting phases
  • Class components use lifecycle methods like componentDidMount, componentDidUpdate
  • Functional components use useEffect hook for lifecycle management
  • Cleanup is crucial to prevent memory leaks and unwanted side effects
  • Dependency arrays in useEffect control when effects run
  • Error boundaries handle errors during rendering
  • Best practices include proper cleanup and avoiding infinite loops
  • Understanding lifecycle is essential for managing side effects and component behavior

Interview angle

  • “Map the class lifecycle to hooks.” - componentDidMount and componentDidUpdate to useEffect with the right dependencies; componentWillUnmount to the effect’s cleanup return. Note it is not a literal translation: an effect is one synchronisation with cleanup, not three separate callbacks.
  • “Why does my effect run twice on mount in development?” - Strict Mode deliberately mounts, unmounts and remounts to expose effects that lack cleanup. It does not happen in production, and the correct response is to add cleanup rather than to disable Strict Mode.
  • “When do you still need a class?” - error boundaries, which have no hook equivalent, though most people use react-error-boundary rather than writing one. Everything else is function components.
  • useEffect or useLayoutEffect?” - useLayoutEffect runs synchronously after DOM mutation and before paint, so use it only for measurement that would otherwise flash. It blocks paint, and it does not run on the server.