Skip to main content
Saved

Component Lifecycle

Category Components

Level Beginner

Examples

React, Vue, Svelte, Native web

Manage setup, updates, and cleanup phases of component existence.

In this article
  1. Problem
  2. Solution
  3. Example
  4. Basic Lifecycle with Cleanup
  5. Handling Multiple Lifecycle Concerns
  6. Preventing Race Conditions with Abort Controllers
  7. Conditional Effects
  8. Benefits
  9. Tradeoffs
  10. Summary
  11. See also

Component Lifecycle

Problem

Navigate around your application for a while, check memory usage, and you’ll often find it still climbing. The usual causes are event listeners added but never removed, subscriptions opened but never closed, and timers still running for components that no longer exist.

I’ve seen applications where navigating 50 pages created 500 event listeners because components kept adding listeners without ever removing them. Click handlers fired three times per click, memory grew continuously, and eventually the browser became unusable.

Race conditions are another symptom: request A starts, request B starts for newer data, but A finishes first and overwrites the fresher data with stale information. Without proper lifecycle management, you can’t abort the stale request before it overwrites the newer data.

Solution

Whatever a component sets up needs undoing at the right point in its lifecycle, whether that’s a listener to remove, a subscription to close, or a timer to clear.

On mount, a component sets up event listeners, starts subscriptions, fetches initial data, and initializes external resources. On unmount, it reverses all of that by removing listeners, canceling subscriptions, clearing timers, and aborting pending requests. Without that cleanup, the application leaks memory.

When props change, the component cleans up resources tied to the old values and sets up new ones. If the user ID changes, it unsubscribes from the old user’s data stream and subscribes to the new one.

Modern frameworks provide lifecycle hooks for this: React’s useEffect with cleanup functions, Vue’s onMounted and onUnmounted, Svelte’s $effect with a returned cleanup function. These hooks run at the right moments for you, which is easier and less error-prone than tracking the lifecycle by hand.

Example

These examples show lifecycle management across frameworks: setting things up on mount, cleaning them up on unmount, and handling dependency changes.

Basic Lifecycle with Cleanup

import { useEffect, useState } from 'react';

function DataSubscriber({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const subscription = dataSource.subscribe(userId, setData);

    // Cleanup runs on unmount or before re-running effect
    return () => subscription.unsubscribe();
  }, [userId]);

  return <div>{data ? data.name : 'Loading...'}</div>;
}

Handling Multiple Lifecycle Concerns

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

  useEffect(() => {
    const subscription = dataSource.subscribe(userId, setUser);
    const interval = setInterval(() => console.log('Polling'), 5000);
    const handleVisibilityChange = () => console.log('Visibility:', document.visibilityState);
    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      subscription.unsubscribe();
      clearInterval(interval);
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [userId]);

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

Preventing Race Conditions with Abort Controllers

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setResults)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => controller.abort();
  }, [query]);

  return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}

Conditional Effects

function ConditionalEffect({ shouldSubscribe, userId }) {
  const [data, setData] = useState(null);

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

    const subscription = dataSource.subscribe(userId, setData);
    return () => subscription.unsubscribe();
  }, [shouldSubscribe, userId]);

  return <div>{data ? data.name : 'Not subscribed'}</div>;
}

Benefits

  • Proper cleanup stops memory leaks, so the application doesn’t keep using more memory the longer someone uses it.
  • Aborting stale requests when dependencies change stops old data from overwriting newer data.
  • Behavior becomes predictable because effects run at well-defined lifecycle points rather than at arbitrary moments.
  • External resources get managed properly: WebSocket connections close, intervals clear, event listeners get removed.
  • Components respond correctly to prop changes. When user ID updates, you unsubscribe from old data and subscribe to new.
  • Debugging becomes easier with side effects centralized in lifecycle hooks instead of spread across the component.

Tradeoffs

  • Forgetting cleanup is easy, and the resulting memory leaks are hard to diagnose because they don’t cause immediate errors.
  • React’s dependency arrays are tricky: mistakes lead to stale closures with outdated values or infinite loops crashing the app.
  • Components with five or more effects become difficult to reason about and maintain.
  • React’s useEffect runs after browser paint. useLayoutEffect runs synchronously before paint, but it delays the paint until it finishes, so it’s best kept for the cases that need it.
  • Cleanup functions must be synchronous. You can’t return a promise, so async cleanup needs different handling.
  • Frameworks have different timing semantics: React’s useEffect usually runs after the browser paints, Vue’s onMounted runs as soon as the component’s DOM has been inserted (before paint), and a custom element’s callbacks run as soon as it’s added to or removed from the document.
  • Testing lifecycle behavior requires careful setup and teardown, because effects from one test can leak into another.

Summary

Component lifecycle defines the stages every component passes through: mounting, updating, and unmounting. Understanding these phases lets you initialize resources when components appear, respond to changes efficiently, and clean up when components disappear. Proper lifecycle management prevents memory leaks and ensures predictable behavior as users navigate your application.

See also

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.