Skip to main content
Saved
Pattern
Difficulty Beginner

Component Lifecycle

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

By Den Odell Added

Component Lifecycle

Problem

Navigate around your application for a while, check memory usage, and you’ll often find it climbing indefinitely. Event listeners added but never removed, subscriptions opened but never closed, timers running against components that no longer exist.

I’ve seen applications where navigating 50 pages created 500 event listeners because components kept adding listeners without cleanup. 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 causes problems.

Solution

Every resource you open needs closing, every listener needs removing, every timer needs clearing, all at exactly the right time in your component’s lifecycle.

On mount, set up event listeners, start subscriptions, fetch initial data, and initialize external resources. On unmount, reverse everything: remove listeners, cancel subscriptions, clear timers, abort pending requests. Skip cleanup, and your application leaks memory.

When props change, clean up resources tied to old values and set up fresh ones for new values. If the user ID changes, unsubscribe from the old user’s data stream and subscribe to the new one.

Modern frameworks provide lifecycle hooks for this: React’s useEffect with cleanup functions, Vue’s onMounted and onUnmounted, Svelte’s onMount and onDestroy. Use these built-in mechanisms rather than managing lifecycle manually.

Example

Here’s 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);
    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=${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 }) {
  useEffect(() => {
    if (!shouldSubscribe) return;

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

  return <div>{/* ... */}</div>;
}

Benefits

  • Memory leaks stop once you clean up properly. Your application can run indefinitely without degradation.
  • Race conditions disappear when you abort stale requests as dependencies change, preventing old data from overwriting fresher data.
  • Behavior becomes predictable because effects run at well-defined lifecycle points rather than scattered randomly.
  • 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 unpredictably.

Tradeoffs

  • Forgetting cleanup is easy, and the resulting memory leaks are subtle. They don’t cause immediate failures, making them hard to diagnose.
  • 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. For synchronous timing before paint, use useLayoutEffect sparingly to avoid blocking the main thread.
  • Cleanup functions must be synchronous. You can’t return a promise, so async cleanup needs different handling.
  • Frameworks have different timing semantics: React effects run async after paint, Vue hooks are synchronous, Web Components have their own rules.
  • Testing lifecycle behavior requires careful setup and teardown; 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.

Newsletter

A Monthly Email
from Den Odell

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

No spam. Unsubscribe anytime.