React Component Lifecycle

From Birth to Death: Understanding Component Phases

Overview

Welcome to our exploration of the React component lifecycle! Just like living beings, React components have a lifecycle - they are born (mounted), they live (update), and eventually they die (unmount). Today, we'll understand these phases and learn how to hook into them effectively using both class components and modern hooks.

The Component Lifecycle Phases

Think of a component's lifecycle like the life of a plant: it's planted (mounted), grows and changes (updates), and eventually withers (unmounts). React provides us with specific moments where we can interact with components during these phases.

graph TD A[Component Lifecycle] --> B[Mounting] A --> C[Updating] A --> D[Unmounting] B --> E[constructor] B --> F[render] B --> G[componentDidMount] C --> H[shouldComponentUpdate] C --> I[render] C --> J[componentDidUpdate] D --> K[componentWillUnmount] style B fill:#9f9,stroke:#333,stroke-width:2px style C fill:#ff9,stroke:#333,stroke-width:2px style D fill:#f99,stroke:#333,stroke-width:2px

Lifecycle in Class Components

Before hooks, class components were the only way to access lifecycle methods. Let's understand these traditional lifecycle methods:

1. Mounting Phase


class LifecycleDemo extends React.Component {
    constructor(props) {
        super(props);
        console.log('1. Constructor: Component is being created');
        this.state = {
            data: null,
            loading: true
        };
    }

    componentDidMount() {
        console.log('3. ComponentDidMount: Component was just mounted');
        // Perfect place for API calls, subscriptions
        this.fetchData();
    }

    fetchData = async () => {
        try {
            const response = await fetch('/api/data');
            const data = await response.json();
            this.setState({ data, loading: false });
        } catch (error) {
            this.setState({ error, loading: false });
        }
    }

    render() {
        console.log('2. Render: Component is rendering');
        const { data, loading, error } = this.state;

        if (loading) return 
Loading...
; if (error) return
Error: {error.message}
; return (

Data: {data}

); } }

2. Updating Phase


class UpdateDemo extends React.Component {
    state = {
        count: 0,
        lastUpdate: null
    };

    shouldComponentUpdate(nextProps, nextState) {
        console.log('Should Update?: Deciding whether to re-render');
        // Optimize by preventing unnecessary renders
        return nextState.count !== this.state.count;
    }

    componentDidUpdate(prevProps, prevState) {
        console.log('Did Update: Component just updated');
        // Handle side effects after updates
        if (prevState.count !== this.state.count) {
            this.setState({ lastUpdate: new Date().toLocaleTimeString() });
        }
    }

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

    render() {
        return (
            

Count: {this.state.count}

Last updated: {this.state.lastUpdate}

); } }

3. Unmounting Phase


class UnmountDemo extends React.Component {
    componentDidMount() {
        // Set up subscription
        this.subscription = subscribeToData(data => {
            this.setState({ data });
        });

        // Start interval
        this.interval = setInterval(() => {
            this.setState({ time: new Date().toLocaleTimeString() });
        }, 1000);
    }

    componentWillUnmount() {
        console.log('Will Unmount: Cleaning up before removal');
        // Clean up to prevent memory leaks
        this.subscription.unsubscribe();
        clearInterval(this.interval);
    }

    render() {
        return 
Current time: {this.state.time}
; } }

Lifecycle with Hooks

With hooks, we don't have named lifecycle methods, but we can achieve the same functionality:

graph LR A[useState] --> B[Component State] C[useEffect] --> D[Side Effects] D --> E[componentDidMount] D --> F[componentDidUpdate] D --> G[componentWillUnmount] H[useLayoutEffect] --> I[Synchronous Effects] J[useRef] --> K[Instance Variables] style C fill:#9cf,stroke:#333,stroke-width:2px style H fill:#fcf,stroke:#333,stroke-width:2px

Mapping Class Lifecycle to Hooks


// Class Component Lifecycle
class LifecycleClass extends React.Component {
    state = { data: null };

    componentDidMount() {
        fetchData().then(data => {
            this.setState({ data });
        });
    }

    componentDidUpdate(prevProps) {
        if (prevProps.id !== this.props.id) {
            fetchData(this.props.id).then(data => {
                this.setState({ data });
            });
        }
    }

    componentWillUnmount() {
        cancelDataFetch();
    }

    render() {
        return 
{this.state.data}
; } } // Equivalent with Hooks function LifecycleHooks({ id }) { const [data, setData] = useState(null); useEffect(() => { // componentDidMount & componentDidUpdate fetchData(id).then(setData); // componentWillUnmount return () => { cancelDataFetch(); }; }, [id]); // Dependencies return
{data}
; }

Advanced Lifecycle Patterns

1. Error Boundaries


class ErrorBoundary extends React.Component {
    state = { hasError: false, error: null };

    static getDerivedStateFromError(error) {
        // Update state to show fallback UI
        return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
        // Log error to error reporting service
        console.error('Error caught:', error, errorInfo);
        logErrorToService(error, errorInfo);
    }

    render() {
        if (this.state.hasError) {
            return (
                

Something went wrong.

Error Details

{this.state.error.toString()}

); } return this.props.children; } } // Usage <ErrorBoundary> <ComponentThatMightError /> </ErrorBoundary>

2. Conditional Effects


function SearchComponent({ query, category }) {
    const [results, setResults] = useState([]);
    const [isSearching, setIsSearching] = useState(false);

    // Effect only runs when query changes
    useEffect(() => {
        if (!query) return;

        setIsSearching(true);
        searchByQuery(query).then(data => {
            setResults(data);
            setIsSearching(false);
        });
    }, [query]);

    // Separate effect for category changes
    useEffect(() => {
        if (!category) return;

        filterByCategory(category).then(filtered => {
            setResults(filtered);
        });
    }, [category]);

    return (
        
{isSearching ? ( <LoadingSpinner /> ) : ( <ResultsList results={results} /> )}
); }

3. Lifecycle with Data Fetching


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

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

        async function fetchUserData() {
            try {
                setLoading(true);
                const [userData, userPosts] = await Promise.all([
                    fetch(`/api/users/${userId}`).then(r => r.json()),
                    fetch(`/api/users/${userId}/posts`).then(r => r.json())
                ]);

                if (mounted) {
                    setUser(userData);
                    setPosts(userPosts);
                    setLoading(false);
                }
            } catch (error) {
                if (mounted) {
                    console.error('Failed to fetch user data:', error);
                    setLoading(false);
                }
            }
        }

        fetchUserData();

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

    if (loading) return <LoadingSpinner />;
    if (!user) return 
User not found
; return (
<UserInfo user={user} /> <UserPosts posts={posts} />
); }

Lifecycle Optimization Techniques

1. useMemo for Expensive Computations


function ExpensiveComponent({ items, filter }) {
    // Only recompute when items or filter changes
    const filteredItems = useMemo(() => {
        console.log('Filtering items...');
        return items.filter(item => 
            item.name.toLowerCase().includes(filter.toLowerCase())
        );
    }, [items, filter]);

    const totalValue = useMemo(() => {
        console.log('Calculating total...');
        return filteredItems.reduce((sum, item) => sum + item.value, 0);
    }, [filteredItems]);

    return (
        
Total Value: ${totalValue}
    {filteredItems.map(item => (
  • {item.name}: ${item.value}
  • ))}
); }

2. useCallback for Stable Functions


function ParentComponent() {
    const [count, setCount] = useState(0);
    const [text, setText] = useState('');

    // This function is recreated on every render
    const unstableIncrement = () => {
        setCount(c => c + 1);
    };

    // This function is stable across renders
    const stableIncrement = useCallback(() => {
        setCount(c => c + 1);
    }, []); // No dependencies, so never recreated

    return (
      <div>
          <input 
              value={text} 
              onChange={e => setText(e.target.value)} 
          />
          <ChildComponent onIncrement={stableIncrement} />
      </div>
  );
  }
  

const ChildComponent = React.memo(({ onIncrement }) => {
    console.log('Child rendered');
    return ;
});
            

Common Lifecycle Patterns

1. Auto-save with Debounce


function AutoSaveEditor() {
    const [content, setContent] = useState('');
    const [saveStatus, setSaveStatus] = useState('saved');

    useEffect(() => {
        if (!content) return;

        setSaveStatus('saving...');
        const timeoutId = setTimeout(() => {
            saveToServer(content).then(() => {
                setSaveStatus('saved');
            });
        }, 1000); // Debounce delay

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

    return (
    <div>
        <textarea
            value={content}
            onChange={e => setContent(e.target.value)}
            placeholder="Start typing..."
        />
        <div>Status: {saveStatus}</div>
    </div>
);
}

            

2. Window Event Listeners


function WindowEventComponent() {
    const [windowSize, setWindowSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });
    const [scrollPosition, setScrollPosition] = useState(0);

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

        function handleScroll() {
            setScrollPosition(window.pageYOffset);
        }

        window.addEventListener('resize', handleResize);
        window.addEventListener('scroll', handleScroll);

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

    return (
        

Window: {windowSize.width} x {windowSize.height}

Scroll: {scrollPosition}px

); }

3. Intersection Observer


function LazyImage({ src, alt }) {
    const [isVisible, setIsVisible] = useState(false);
    const imgRef = useRef();

    useEffect(() => {
        const observer = new IntersectionObserver(
            ([entry]) => {
                if (entry.isIntersecting) {
                    setIsVisible(true);
                    observer.disconnect();
                }
            },
            { threshold: 0.1 }
        );

        if (imgRef.current) {
            observer.observe(imgRef.current);
        }

        return () => {
            observer.disconnect();
        };
    }, []);

    return (
        
{isVisible ? ( {alt} ) : (
Loading...
)}
); }

Lifecycle Best Practices

graph TD A[Lifecycle Best Practices] --> B[Clean Up Effects] A --> C[Avoid Memory Leaks] A --> D[Handle Race Conditions] A --> E[Optimize Performance] B --> F[Remove Event Listeners] B --> G[Cancel Subscriptions] B --> H[Clear Timers] C --> I[Check Component Mount Status] C --> J[Cancel Pending Requests] D --> K[Use Cleanup Functions] D --> L[Implement AbortController] E --> M[Memoize Expensive Operations] E --> N[Use React.memo] E --> O[Avoid Unnecessary Renders] style A fill:#f9f,stroke:#333,stroke-width:2px

Lifecycle Debugging


function useLifecycleLogger(componentName) {
    const mounted = useRef(false);

    useEffect(() => {
        console.log(`${componentName} mounted`);
        mounted.current = true;

        return () => {
            console.log(`${componentName} will unmount`);
            mounted.current = false;
        };
    }, [componentName]);

    useEffect(() => {
        if (mounted.current) {
            console.log(`${componentName} updated`);
        }
    });

    return mounted;
}

// Usage
function MyComponent(props) {
    const mounted = useLifecycleLogger('MyComponent');
    
    return (
        
{/* component content */}
); }

Practice Exercises

Exercise 1: Data Fetching Component

Create a component that fetches data on mount and handles loading, error, and cleanup states:


function DataFetcher({ endpoint }) {
    // Implement:
    // - Fetch data on mount
    // - Re-fetch when endpoint changes
    // - Show loading state
    // - Handle errors
    // - Cancel pending requests on unmount
}
            

Exercise 2: Animation Component

Build a component that animates when it enters/exits the viewport:


function AnimatedSection({ children }) {
    // Implement:
    // - Use Intersection Observer
    // - Add CSS classes for animation
    // - Clean up observer on unmount
    // - Handle multiple instances
}
            

Exercise 3: Form with Auto-save

Create a form that auto-saves changes with debouncing:


function AutoSaveForm() {
    // Implement:
    // - Track form changes
    // - Debounce save operations
    // - Show save status
    // - Handle errors
    // - Clean up pending saves
}
            

Key Takeaways

What's Next?

Tomorrow, we'll dive into handling events in React and learn how to create interactive user interfaces with forms, buttons, and other interactive elements!

Homework