Describe the main phases of the React component lifecycle and the corresponding methods for class and functional components

👨‍💻 Frontend Developer 🟢 Almost Certain 🎚️ Hard
#React

Brief Answer

React component lifecycle consists of four main phases:

Mounting — component is created and inserted into the DOM

Updating — component is re-rendered when props or state change

Unmounting — component is removed from the DOM

Error Handling — handling errors in components

PhaseClass ComponentsFunctional Components
Mountingconstructor, render, componentDidMountuseEffect(() => {}, [])
UpdatingcomponentDidUpdateuseEffect(() => {}, [deps])
UnmountingcomponentWillUnmountuseEffect(() => { return () => {} }, [])
ErrorscomponentDidCatch, getDerivedStateFromErrorNo built-in support

Key rule: Use functional components with hooks for new projects! 🎯


Full Answer

Imagine a component is an actor on stage. It has time to come on stage (mounting), time to change costumes (updating), time to leave the stage (unmounting) and time when it might trip (error handling)! 🎭

What is component lifecycle

Lifecycle is a series of stages that a component goes through from creation to destruction:

// Simple lifecycle example
function SimpleComponent() {
  console.log('1. Component render');
  
  useEffect(() => {
    console.log('2. Component mounted');
    
    return () => {
      console.log('3. Component will unmount');
    };
  }, []);
  
  return <div>Simple component</div>;
}

Phase 1: Mounting

Mounting is when a component is created and inserted into the DOM.

Class Components

class MountingExample extends Component {
  constructor(props) {
    super(props);
    console.log('1. constructor');
    this.state = { count: 0 };
  }
  
  static getDerivedStateFromProps(props, state) {
    console.log('2. getDerivedStateFromProps');
    return null; // Don't change state
  }
  
  componentDidMount() {
    console.log('3. componentDidMount');
    // Subscriptions, API requests
  }
  
  render() {
    console.log('4. render');
    return <div>Component mounted</div>;
  }
}

Functional Components

function MountingExample() {
  console.log('1. Component render');
  
  // useEffect with empty dependency array = componentDidMount
  useEffect(() => {
    console.log('2. useEffect (equivalent to componentDidMount)');
    
    // Cleanup (equivalent to componentWillUnmount)
    return () => {
      console.log('3. useEffect cleanup');
    };
  }, []); // Empty dependency array
  
  return <div>Component mounted</div>;
}

Phase 2: Updating

Updating occurs when a component is re-rendered due to changes in props or state.

Class Components

class UpdatingExample extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  static getDerivedStateFromProps(props, state) {
    console.log('1. getDerivedStateFromProps');
    return null;
  }
  
  shouldComponentUpdate(nextProps, nextState) {
    console.log('2. shouldComponentUpdate');
    // Optimization: return false to skip update
    return true;
  }
  
  componentDidUpdate(prevProps, prevState) {
    console.log('3. componentDidUpdate');
    // Work with DOM after update
  }
  
  render() {
    console.log('4. render');
    return (
      <div>
        <p>Counter: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increase
        </button>
      </div>
    );
  }
}

Functional Components

function UpdatingExample() {
  const [count, setCount] = useState(0);
  
  console.log('1. Component render');
  
  // useEffect with dependencies = componentDidUpdate
  useEffect(() => {
    console.log('2. useEffect (equivalent to componentDidUpdate)');
    // Effects on update
    
    // Cleanup before next effect
    return () => {
      console.log('3. Cleanup previous effect');
    };
  }, [count]); // Dependency on count
  
  return (
    <div>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
}

Phase 3: Unmounting

Unmounting is when a component is removed from the DOM.

Class Components

class UnmountingExample extends Component {
  componentDidMount() {
    // Create subscription
    this.timer = setInterval(() => {
      console.log('Timer running');
    }, 1000);
  }
  
  componentWillUnmount() {
    console.log('componentWillUnmount: Resource cleanup');
    // Mandatory cleanup!
    clearInterval(this.timer);
  }
  
  render() {
    return <div>Component will be removed soon</div>;
  }
}
 
// Usage with conditional rendering
function App() {
  const [showComponent, setShowComponent] = useState(true);
  
  return (
    <div>
      <button onClick={() => setShowComponent(!showComponent)}>
        {showComponent ? 'Hide' : 'Show'} component
      </button>
      
      {showComponent && <UnmountingExample />}
    </div>
  );
}

Functional Components

function UnmountingExample() {
  useEffect(() => {
    // Create subscription
    const timer = setInterval(() => {
      console.log('Timer running');
    }, 1000);
    
    // Cleanup function (equivalent to componentWillUnmount)
    return () => {
      console.log('Cleanup: Stopping timer');
      clearInterval(timer);
    };
  }, []); // Empty array = only on mount
  
  return <div>Component will be removed soon</div>;
}
 
// Usage with conditional rendering
function App() {
  const [showComponent, setShowComponent] = useState(true);
  
  return (
    <div>
      <button onClick={() => setShowComponent(!showComponent)}>
        {showComponent ? 'Hide' : 'Show'} component
      </button>
      
      {showComponent && <UnmountingExample />}
    </div>
  );
}

Phase 4: Error Handling

Error handling allows catching errors in child components.

Class Components

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  // Catch errors in child components
  static getDerivedStateFromError(error) {
    console.log('getDerivedStateFromError:', error);
    // Update state to show fallback UI
    return { hasError: true };
  }
  
  // Log errors
  componentDidCatch(error, errorInfo) {
    console.log('componentDidCatch:', error, errorInfo);
    // Can send error to logging service
  }
  
  render() {
    if (this.state.hasError) {
      // Fallback UI
      return <h1>Something went wrong!</h1>;
    }
    
    return this.props.children;
  }
}
 
// Usage
function App() {
  return (
    <ErrorBoundary>
      <BuggyComponent />
    </ErrorBoundary>
  );
}
 
function BuggyComponent() {
  throw new Error('Error in component!');
}

Functional Components

// Functional components have no built-in error handling support
// Must use class-based Error Boundary
 
function BuggyComponent() {
  throw new Error('Error in component!');
}
 
// Wrap in class-based Error Boundary
function App() {
  return (
    <ErrorBoundary>
      <BuggyComponent />
    </ErrorBoundary>
  );
}

Practical Examples

1. Loading data on mount

// Class component
class UserProfileClass extends Component {
  constructor(props) {
    super(props);
    this.state = { user: null, loading: true };
  }
  
  async componentDidMount() {
    try {
      const user = await fetchUser(this.props.userId);
      this.setState({ user, loading: false });
    } catch (error) {
      this.setState({ loading: false });
    }
  }
  
  render() {
    const { user, loading } = this.state;
    
    if (loading) return <div>Loading...</div>;
    if (!user) return <div>User not found</div>;
    
    return <div>Hello, {user.name}!</div>;
  }
}
 
// Functional component
function UserProfileFunction({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const loadUser = async () => {
      try {
        const userData = await fetchUser(userId);
        setUser(userData);
      } catch (error) {
        // Handle error
      } finally {
        setLoading(false);
      }
    };
    
    loadUser();
  }, [userId]); // Dependency on userId
  
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;
  
  return <div>Hello, {user.name}!</div>;
}

2. Event subscription

// Class component
class WindowSizeClass extends Component {
  constructor(props) {
    super(props);
    this.state = {
      width: window.innerWidth,
      height: window.innerHeight
    };
  }
  
  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }
  
  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }
  
  handleResize = () => {
    this.setState({
      width: window.innerWidth,
      height: window.innerHeight
    });
  };
  
  render() {
    return (
      <div>
        Window size: {this.state.width} x {this.state.height}
      </div>
    );
  }
}
 
// Functional component
function WindowSizeFunction() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup on unmount
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty array = only on mount
  
  return (
    <div>
      Window size: {size.width} x {size.height}
    </div>
  );
}

Common Mistakes

1. Forgotten resource cleanup

// ❌ Problem: memory leaks
function BadComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer');
    }, 1000);
    
    // Forgot cleanup!
  }, []);
  
  return <div>Component with memory leak</div>;
}
 
// ✅ Solution: proper cleanup
function GoodComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer');
    }, 1000);
    
    // Mandatory cleanup
    return () => {
      clearInterval(timer);
    };
  }, []);
  
  return <div>Component without leaks</div>;
}

2. Incorrect dependencies in useEffect

// ❌ Problem: infinite loop
function BadComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [user]); // Incorrect dependency!
  
  return <div>{user?.name}</div>;
}
 
// ✅ Solution: correct dependencies
function GoodComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // Correct dependency
  
  return <div>{user?.name}</div>;
}

Best Practices

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  // Related effects for one data source
  useEffect(() => {
    const loadUserData = async () => {
      const userData = await fetchUser(userId);
      const userPosts = await fetchUserPosts(userId);
      
      setUser(userData);
      setPosts(userPosts);
    };
    
    loadUserData();
  }, [userId]);
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <div>{posts.length} posts</div>
    </div>
  );
}

2. Performance optimization

// Using useMemo and useCallback
function OptimizedComponent({ userId, onUserSelect }) {
  const [user, setUser] = useState(null);
  
  // Memoizing calculations
  const userDisplayName = useMemo(() => {
    return user ? `${user.firstName} ${user.lastName}` : 'Guest';
  }, [user]);
  
  // Memoizing callbacks
  const handleSelect = useCallback(() => {
    onUserSelect(userId);
  }, [onUserSelect, userId]);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  return (
    <div>
      <h1>{userDisplayName}</h1>
      <button onClick={handleSelect}>Select user</button>
    </div>
  );
}

When to Use Each Approach

Class Components

// Use only if:
// 1. Maintaining legacy code
// 2. Need error handling (Error Boundaries)
// 3. Working with React version < 16.8
 
class LegacyComponent extends Component {
  // ... implementation with lifecycle methods
}

Functional Components

// ✅ Use always for new projects
function ModernComponent() {
  // ... implementation with hooks
}

Summary

Component lifecycle is like a play script, where each actor (component) has time to come on stage, change costumes, leave the stage and be rescued when falling! 🎭

  • Mounting — component birth ✨
  • Updating — component life 🔄
  • Unmounting — component death ⚰️
  • Errors — component insurance 🛡️

Class Components:

  • Many lifecycle methods
  • this.state and this.props
  • Error Boundaries support

Functional Components:

  • One useEffect hook for everything
  • useState for state
  • More readable code

Practical tips:

  1. Always clean up resources in useEffect
  2. Specify dependencies correctly
  3. Use functional components
  4. Group related effects

Lifecycle is the foundation of understanding React! 💪


Want more useful React articles? Subscribe to EasyAdvice, bookmark the site and level up every day! 🚀