Published on

React.js Component Lifecycle Guide

Authors

React Component Lifecycle Phases

Class Components Lifecycle

class ExampleComponent extends React.Component {
  // Mounting Phase
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    console.log('1. Constructor called');
  }

  static getDerivedStateFromProps(props, state) {
    console.log('2. getDerivedStateFromProps called');
    return null;
  }

  componentDidMount() {
    console.log('4. componentDidMount called');
    // Good for: API calls, subscriptions, DOM manipulations
  }

  // Updating Phase
  shouldComponentUpdate(nextProps, nextState) {
    console.log('5. shouldComponentUpdate called');
    return true;
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('6. getSnapshotBeforeUpdate called');
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('7. componentDidUpdate called');
    // Good for: Network requests based on prop changes
  }

  // Unmounting Phase
  componentWillUnmount() {
    console.log('8. componentWillUnmount called');
    // Good for: Cleanup (canceling network requests, subscriptions)
  }

  render() {
    console.log('3. Render called');
    return <div>Component Content</div>;
  }
}

Functional Components with Hooks

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

function ExampleFunctionalComponent({ data }) {
  // State initialization (similar to constructor)
  const [count, setCount] = useState(0);

  // Runs after every render
  useEffect(() => {
    console.log('Component did mount/update');

    // Cleanup function (runs before next effect or unmount)
    return () => {
      console.log('Cleanup - similar to componentWillUnmount');
    };
  });

  // Runs only on mount (empty dependency array)
  useEffect(() => {
    console.log('Component did mount');
    return () => {
      console.log('Component will unmount');
    };
  }, []);

  // Runs when 'data' prop changes
  useEffect(() => {
    console.log('Data changed:', data);
  }, [data]);

  // Runs synchronously after DOM mutations
  useLayoutEffect(() => {
    console.log('DOM mutations completed');
  });

  return <div>Count: {count}</div>;
}

Lifecycle Methods vs Hooks Comparison

// Class Component Lifecycle
class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
  }

  componentDidMount() {
    fetchData();
  }

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

  componentWillUnmount() {
    cleanup();
  }
}

// Equivalent Functional Component with Hooks
function FunctionalComponent({ id }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData();

    return () => cleanup();
  }, [id]);
}

Common Use Cases and Best Practices

Data Fetching

function DataFetchingComponent({ id }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(`/api/data/${id}`);
        const result = await response.json();
        
        if (mounted) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (mounted) {
          setError(err.message);
        }
      } finally {
        if (mounted) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      mounted = false;
    };
  }, [id]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>Data: {JSON.stringify(data)}</div>;
}

Event Listeners

function EventListenerComponent() {
  useEffect(() => {
    function handleScroll() {
      console.log('Window scrolled');
    }

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return <div style={{ height: '200vh' }}>Scroll me</div>;
}

Subscription Management

function SubscriptionComponent({ channelId }) {
  useEffect(() => {
    const subscription = subscribeToChannel(channelId, {
      onMessage: (message) => {
        console.log('New message:', message);
      },
      onError: (error) => {
        console.error('Subscription error:', error);
      }
    });

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

Common Lifecycle Patterns

Conditional Fetching

function ConditionalFetchComponent({ shouldFetch, id }) {
  useEffect(() => {
    if (!shouldFetch) return;

    const fetchData = async () => {
      // Fetch logic
    };

    fetchData();
  }, [shouldFetch, id]);
}

Debounced Updates

function DebouncedComponent({ searchTerm }) {
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      // Perform search
      console.log('Searching for:', searchTerm);
    }, 500);

    return () => clearTimeout(timeoutId);
  }, [searchTerm]);
}

Performance Optimization

Using Memo

const MemoizedComponent = React.memo(function MyComponent({ data }) {
  return <div>{data.text}</div>;
}, (prevProps, nextProps) => {
  return prevProps.data.id === nextProps.data.id;
});

Callback Memoization

function CallbackComponent() {
  const [count, setCount] = useState(0);

  const memoizedCallback = useCallback(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <ExpensiveChild onCallback={memoizedCallback} />
  );
}

Error Handling

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

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

Custom Hooks for Lifecycle Management

function useMount(callback) {
  useEffect(() => {
    callback();
  }, []);
}

function useUnmount(callback) {
  useEffect(() => {
    return () => callback();
  }, []);
}

function useUpdateEffect(callback, dependencies) {
  const firstRenderRef = useRef(true);

  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false;
      return;
    }
    return callback();
  }, dependencies);
}

Testing Lifecycle Methods

import { render, act } from '@testing-library/react';

test('component lifecycle', async () => {
  const { rerender, unmount } = render(<MyComponent id={1} />);

  // Test mount
  expect(screen.getByText('Initial State')).toBeInTheDocument();

  // Test update
  await act(async () => {
    rerender(<MyComponent id={2} />);
  });

  // Test unmount
  unmount();
});

Remember:

  • Use useEffect for side effects
  • Clean up subscriptions and listeners
  • Handle async operations properly
  • Consider performance implications
  • Test lifecycle behavior thoroughly

Learning Resources

Official Documentation

Online Courses & Tutorials

Tools

Testing Resources