Skip to content

Component Lifecycle — Mounting, Updating, and Unmounting Phases

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Component Lifecycle. We cover key concepts, practical examples, and best practices to help you master this topic.

Component lifecycle in SPAs has three phases — mount (birth), update (change), and unmount (death) — each with hooks for side effects, data fetching, and cleanup operations.

What You'll Learn

By the end of this tutorial, you will understand the component lifecycle phases in React and Vue, how to use lifecycle hooks, and common patterns for data fetching, subscriptions, and cleanup.

Why It Matters

Understanding the component lifecycle prevents bugs like memory leaks (forgetting to unsubscribe), race conditions (fetching data on every render), and unnecessary work (not optimizing re-renders). Every SPA developer must understand when and why components update.

Real-World Use

A chat SPA subscribes to a Websocket connection when the chat component mounts. When the component unmounts (user navigates away), the subscription is cleaned up. Without proper lifecycle handling, the WebSocket connection persists, causing memory leaks and unnecessary network traffic.

Lifecycle Phases

Component Lifecycle
    ┌──────────────────────────────────────────────────────────────┐
    │  MOUNT (birth)                                              │
    │  1. Constructor / useState initialization                   │
    │  2. Render (first time)                                     │
    │  3. DOM insertion                                           │
    │  4. useEffect (layout effects) / componentDidMount           │
    │     → Fetch data, subscribe, set timers                     │
    └──────────────────────────────────────────────────────────────┘
         ↓ (props or state change)
    ┌──────────────────────────────────────────────────────────────┐
    │  UPDATE (life changes)                                      │
    │  1. New props / setState / forceUpdate                       │
    │  2. Re-render                                               │
    │  3. useEffect (if deps changed) / componentDidUpdate         │
    │     → React to changes, re-fetch, update subscriptions      │
    └──────────────────────────────────────────────────────────────┘
         ↓ (component removed from tree)
    ┌──────────────────────────────────────────────────────────────┐
    │  UNMOUNT (death)                                            │
    │  1. Cleanup function from useEffect                         │
    │  2. componentWillUnmount                                     │
    │     → Remove listeners, clear timers, cancel requests       │
    └──────────────────────────────────────────────────────────────┘

Think of component lifecycle like a house. Mount is when you move in — you set up furniture (initialize state), install utilities (fetch data), and set up internet (subscribe). Update is when you redecorate or get new furniture (state/props change). Unmount is when you move out — you cancel utilities and clean up.

React Lifecycle with Hooks

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

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

    // MOUNT: Fetch user data when component first renders
    // Also runs when userId changes (UPDATE)
    useEffect(() => {
        let cancelled = false;
        cancelRequest.current = false;

        async function fetchUser() {
            setLoading(true);
            setError(null);

            try {
                const response = await fetch(`/api/users/${userId}`);
                if (!response.ok) throw new Error('User not found');

                const data = await response.json();
                if (!cancelled) {
                    setUser(data);
                    setLoading(false);
                }
            } catch (err) {
                if (!cancelled) {
                    setError(err.message);
                    setLoading(false);
                }
            }
        }

        fetchUser();

        // CLEANUP: Cancel request if component unmounts
        return () => {
            cancelled = true;
            console.log('Cleanup: cancelled fetch for user', userId);
        };
    }, [userId]); // Only re-run when userId changes

    // MOUNT: Set up event listener
    useEffect(() => {
        function handleOnline() {
            console.log('User is back online, refreshing data');
        }

        window.addEventListener('online', handleOnline);

        // CLEANUP: Remove listener on unmount
        return () => {
            window.removeEventListener('online', handleOnline);
        };
    }, []); // Empty deps = run once on mount, cleanup on unmount

    // MOUNT: Start polling timer
    useEffect(() => {
        const interval = setInterval(() => {
            console.log('Polling for updates...');
        }, 30000);

        // CLEANUP: Clear timer on unmount
        return () => {
            clearInterval(interval);
            console.log('Cleanup: polling stopped');
        };
    }, []);

    if (loading) return <div>Loading user...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!user) return <div>User not found</div>;

    return (
        <div>
            <h2>{user.name}</h2>
            <p>Email: {user.email}</p>
            <p>Role: {user.role}</p>
        </div>
    );
}

Common Lifecycle Patterns

// Pattern 1: Fetch on mount
function DataLoader({ url }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        let cancelled = false;

        fetch(url)
            .then(res => res.json())
            .then(result => {
                if (!cancelled) {
                    setData(result);
                    setLoading(false);
                }
            })
            .catch(() => {
                if (!cancelled) setLoading(false);
            });

        return () => { cancelled = true; };
    }, [url]);

    if (loading) return <div>Loading...</div>;
    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

// Pattern 2: Subscribe and unsubscribe
function useOnlineStatus() {
    const [isOnline, setIsOnline] = useState(navigator.onLine);

    useEffect(() => {
        function handleOnline() { setIsOnline(true); }
        function handleOffline() { setIsOnline(false); }

        window.addEventListener('online', handleOnline);
        window.addEventListener('offline', handleOffline);

        return () => {
            window.removeEventListener('online', handleOnline);
            window.removeEventListener('offline', handleOffline);
        };
    }, []);

    return isOnline;
}

// Pattern 3: Debounced input
function useDebounce(value, delay = 300) {
    const [debounced, setDebounced] = useState(value);

    useEffect(() => {
        const timer = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(timer);
    }, [value, delay]);

    return debounced;
}

// Pattern 4: Track previous value
function usePrevious(value) {
    const ref = useRef();

    useEffect(() => {
        ref.current = value;
    }, [value]);

    return ref.current;
}

Vue Lifecycle Hooks

// Vue 3 Composition API lifecycle
import { ref, onMounted, onUpdated, onUnmounted, watch } from 'vue';

export default {
    setup(props) {
        const user = ref(null);
        const loading = ref(true);
        const error = ref(null);

        // Mount: equivalent to useEffect([], [])
        onMounted(async () => {
            try {
                const response = await fetch(`/api/users/${props.userId}`);
                user.value = await response.json();
            } catch (err) {
                error.value = err.message;
            } finally {
                loading.value = false;
            }
        });

        // Update: react to prop changes
        watch(() => props.userId, async (newId) => {
            loading.value = true;
            try {
                const response = await fetch(`/api/users/${newId}`);
                user.value = await response.json();
            } catch (err) {
                error.value = err.message;
            } finally {
                loading.value = false;
            }
        });

        // Unmount: cleanup
        onUnmounted(() => {
            console.log('Component unmounted, cleaning up');
        });

        return { user, loading, error };
    }
};

Common Mistakes

  1. Not cleaning up subscriptions. Forgetting to unsubscribe from WebSockets, timers, or event listeners causes memory leaks. Always return a cleanup function.
  2. Missing dependency arrays. Omitting dependencies in useEffect causes stale closures or infinite loops. Include all values used inside the effect.
  3. Fetching data without cancellation. If the component unmounts before fetch completes, setState on unmounted component throws. Use a cancelled flag.
  4. Running effects on every render. Effects with no dependency array run after every render. Add [] for mount-only or [dep] for conditional execution.
  5. Mutating refs during render. useRef is for mutable values that should not trigger re-renders. Update refs in effects, not during render.

Practice Questions

  1. What are the three main phases of a component lifecycle?
  2. Why should you return a cleanup function from useEffect?
  3. What does the dependency array in useEffect control?
  4. How do you cancel an in-flight API request when a component unmounts?
  5. What is the difference between useEffect and useLayoutEffect?

Challenge: Build a component that: fetches data on mount with cancellation, subscribes to a WebSocket (simulated with setInterval), updates on prop changes, and cleans up all subscriptions on unmount. Log each lifecycle phase.

FAQ

Does useEffect run synchronously or asynchronously?

useEffect runs asynchronously after the browser paints. useLayoutEffect runs synchronously before paint. Most data fetching should use useEffect.

What happens if I omit the dependency array?

The effect runs after every render. This is usually a bug. Always specify dependencies to control when the effect runs.

Can I have multiple useEffect hooks?

Yes. Separate different concerns into different effects. Fetching data, subscribing, and logging should each have their own useEffect.

How do lifecycle hooks differ between React and Vue?

React uses useEffect and its cleanup function. Vue 3 uses onMounted, onUpdated, onUnmounted. Both serve the same purpose with different APIs.

Does StrictMode affect lifecycle in development?

Yes. React StrictMode double-invokes effects in development to help detect missing cleanup. Effects run twice, emphasizing the need for proper cleanup.

Mini Project

Build a dashboard component that: fetches user list on mount, polls for new data every 30 seconds, subscribes to user online/offline events, updates the document title with user count, and cleans up all subscriptions and timers on unmount. Log each lifecycle event.

What's Next

You understand component lifecycle. Now learn about code splitting and lazy loading in SPAs to optimize initial load time.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro