Lifecycle Callbacks & Component Communication

Mikey NicholsMikey Nichols
8 min read

In this guide, we'll explore how to build truly framework-agnostic UI components that work anywhereβ€”React, Angular, Vue, or even with no framework at all! We'll deep dive into two critical aspects that separate amateur from professional implementations:

  1. Lifecycle callbacks - The secret sauce that brings your components to life

  2. Component communication - Making your components talk to each other seamlessly

Ready to future-proof your front-end skills? Let's dive in!


Lifecycle Callbacks: The Heartbeat of Your Components

Imagine your Web Component as a living entity with a clear lifecycle: it's born, grows, changes, and eventually leaves the page. Each of these stages triggers specific callback methods that you can tap into:

1. connectedCallback(): The Birthday Party

When your component joins the DOM, it's time to celebrate! This is where initialization happens:

class BirthdayComponent extends HTMLElement {
  connectedCallback() {
    console.log('πŸŽ‰ I\'m alive! Time to party!');
    this.innerHTML = `
      <div class="birthday-card">
        <h2>Hello World!</h2>
        <p>I was born at ${new Date().toLocaleTimeString()}</p>
      </div>
    `;
    // Start any necessary timers, fetch data, etc.
  }
}

customElements.define('birthday-card', BirthdayComponent);

Try it yourself: Add <birthday-card></birthday-card> to your HTML and watch it come to life in the browser console!

2. disconnectedCallback(): The Farewell Tour

All good things come to an end. When your component is removed from the DOM, use this opportunity for a proper cleanup:

class CleanupComponent extends HTMLElement {
  connectedCallback() {
    this.intervalId = setInterval(() => {
      console.log('Still here...');
    }, 1000);
  }

  disconnectedCallback() {
    console.log('πŸ’« It\'s been a pleasure serving you! Cleaning up...');
    clearInterval(this.intervalId);
    // Remove event listeners, close connections, etc.
  }
}

customElements.define('cleanup-demo', CleanupComponent);

Hands-on challenge: Create a component with a button that removes itself after a countdown. Watch the disconnectedCallback fire in your console!

3. attributeChangedCallback(name, oldValue, newValue): The Chameleon Effect

Components need to react to changes. This callback is your component's sense organ:

class MoodRing extends HTMLElement {
  static get observedAttributes() {
    return ['mood'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'mood') {
      console.log(`πŸ”„ Mood changing from ${oldValue || 'unset'} to ${newValue}!`);

      // Update the UI based on the new mood
      const moodEmojis = {
        happy: 'πŸ˜€',
        sad: '😒',
        angry: '😠',
        confused: 'πŸ€”'
      };

      this.innerHTML = `
        <div class="mood-display">
          <span style="font-size: 3rem">${moodEmojis[newValue] || '😐'}</span>
          <p>I'm feeling ${newValue || 'neutral'}!</p>
        </div>
      `;
    }
  }
}

customElements.define('mood-ring', MoodRing);

Interactive example: Click the buttons below to change my mood!

<mood-ring mood="happy"></mood-ring>
<div>
  <button onclick="document.querySelector('mood-ring').setAttribute('mood', 'happy')">Happy</button>
  <button onclick="document.querySelector('mood-ring').setAttribute('mood', 'sad')">Sad</button>
  <button onclick="document.querySelector('mood-ring').setAttribute('mood', 'angry')">Angry</button>
  <button onclick="document.querySelector('mood-ring').setAttribute('mood', 'confused')">Confused</button>
</div>

4. adoptedCallback(): The Adoption Papers

This one's a bit like finding yourself in a new family. When your element moves to a new document:

class AdoptedPet extends HTMLElement {
  adoptedCallback() {
    console.log('🏠 New home! Adjusting to my new document...');
    // Reset state for the new document context
  }
}

customElements.define('adopted-pet', AdoptedPet);

Fun fact: While less commonly used, this callback is crucial for applications that work with iframes or multiple documents!

Web Components in the Wild

Before we dive deeper, let's see who's already embracing this technology:

  • Google uses Web Components across many of their products including YouTube and Google Maps

  • Microsoft has embraced them for their Fluent UI design system

  • Adobe leverages them in their Spectrum design system

  • Even Salesforce built their Lightning Web Components on this technology

Why are these tech giants investing in Web Components? Because they're future-proof, performant, and framework-agnostic!


Component Communication: Let's Talk!

Web Components are fantastic at encapsulation, but isolation isn't always desirable. Here's how to make them communicate effectively:

1. Parent-Child Communication: The Family Chat

Just like in human families, parent and child components can have rich communication:

Parents β†’ Children: Passing Down Wisdom

Parents can pass data to children through attributes or properties:

class ParentComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div class="parent">
        <h2>I'm the parent</h2>
        <child-component message="Be good!"></child-component>
        <button id="change-message">Change Message</button>
      </div>
    `;

    this.querySelector('#change-message').addEventListener('click', () => {
      const child = this.querySelector('child-component');
      child.setAttribute('message', 'Clean your room!');
    });
  }
}

class ChildComponent extends HTMLElement {
  static get observedAttributes() {
    return ['message'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'message') {
      this.querySelector('.message').textContent = newValue;
    }
  }

  connectedCallback() {
    this.innerHTML = `
      <div class="child" style="border: 1px solid blue; padding: 10px; margin: 10px 0;">
        <h3>I'm the child</h3>
        <p>Parent says: <span class="message">${this.getAttribute('message') || 'Nothing yet'}</span></p>
        <button class="respond">Respond to Parent</button>
      </div>
    `;

    this.querySelector('.respond').addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('child-response', {
        bubbles: true,
        detail: { response: 'Okay, I will!' }
      }));
    });
  }
}

customElements.define('parent-component', ParentComponent);
customElements.define('child-component', ChildComponent);

Children β†’ Parents: Speaking Up

Children talk back to parents using custom events:

// In the ParentComponent class
connectedCallback() {
  // ... previous code ...

  this.addEventListener('child-response', (e) => {
    alert(`Child says: ${e.detail.response}`);
  });
}

Try it live: Watch the following demo to see this parent-child communication in action!

2. Sibling Communication: The Playground Talk

Sometimes components at the same level need to coordinate:

class SiblingContainer extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div class="container">
        <h2>Sibling Communication</h2>
        <sibling-sender></sibling-sender>
        <sibling-receiver></sibling-receiver>
      </div>
    `;

    // The parent mediates communication between siblings
    this.addEventListener('message-sent', (e) => {
      const receiver = this.querySelector('sibling-receiver');
      receiver.receiveMessage(e.detail.message);
    });
  }
}

class SiblingSender extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div style="border: 1px solid green; padding: 10px; margin: 10px 0;">
        <h3>Sibling A (Sender)</h3>
        <input type="text" placeholder="Type a message" value="Hello sibling!">
        <button>Send to Sibling B</button>
      </div>
    `;

    this.querySelector('button').addEventListener('click', () => {
      const message = this.querySelector('input').value;
      this.dispatchEvent(new CustomEvent('message-sent', {
        bubbles: true,
        detail: { message }
      }));
    });
  }
}

class SiblingReceiver extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div style="border: 1px solid purple; padding: 10px; margin: 10px 0;">
        <h3>Sibling B (Receiver)</h3>
        <p>Message received: <span class="message">Nothing yet</span></p>
      </div>
    `;
  }

  receiveMessage(message) {
    this.querySelector('.message').textContent = message;
    // Add animation to show the message arrived
    const messageElement = this.querySelector('.message');
    messageElement.style.animation = 'highlight 1s';
    setTimeout(() => { messageElement.style.animation = ''; }, 1000);
  }
}

customElements.define('sibling-container', SiblingContainer);
customElements.define('sibling-sender', SiblingSender);
customElements.define('sibling-receiver', SiblingReceiver);

3. Global Event Bus: The Town Square

For components that are far apart in the DOM tree, a global event bus works like a town square:

// EventBus.js - The town square where everyone meets
const EventBus = new EventTarget();

// Export it so components can import and use it
export { EventBus };

Using the event bus in components:

import { EventBus } from './EventBus.js';

class GlobalSender extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div style="border: 1px solid red; padding: 10px;">
        <h3>Global Announcer</h3>
        <button>Broadcast Announcement</button>
      </div>
    `;

    this.querySelector('button').addEventListener('click', () => {
      EventBus.dispatchEvent(new CustomEvent('global-announcement', {
        detail: { message: 'Attention everyone! Important announcement!' }
      }));
    });
  }
}

class GlobalListener extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div style="border: 1px solid blue; padding: 10px;">
        <h3>Global Listener</h3>
        <p>Last announcement: <span class="announcement">None yet</span></p>
      </div>
    `;

    // Subscribe to global events
    EventBus.addEventListener('global-announcement', this.handleAnnouncement.bind(this));
  }

  disconnectedCallback() {
    // Clean up subscription when element is removed
    EventBus.removeEventListener('global-announcement', this.handleAnnouncement.bind(this));
  }

  handleAnnouncement(event) {
    this.querySelector('.announcement').textContent = event.detail.message;
  }
}

customElements.define('global-sender', GlobalSender);
customElements.define('global-listener', GlobalListener);

Pro tip: This pattern works great for notifications, theme changes, or user authentication status that many components need to know about!


Each action demonstrates a different aspect of Web Components communication!

Check out the live demo below, and explore the code to see how it implements the concepts we've learned.

Common Pitfalls to Avoid

  1. Forgetting to clean up resources in disconnectedCallback() - This is the #1 cause of memory leaks!

  2. Not using Shadow DOM when appropriate - Without it, your CSS might leak out or be affected by the parent page

  3. Overusing attributes for complex data - Consider properties instead for objects and arrays

  4. Ignoring browser compatibility - Always check Can I Use and consider polyfills for older browsers

  5. Creating too many small components - Every component has overhead; find the right balance for your application

Expert Tips

  • Debug attribute changes by adding a console.log in attributeChangedCallback

  • Create a base component class that handles common functionality for all your components

  • Document your components with clear API comments - your future self will thank you!

  • Use Shadow DOM for true encapsulation by adding this.attachShadow({mode: 'open'}) in your constructor

  • Leverage custom events for component communication rather than directly accessing other components

Try It Yourself: Interactive Playground

Want to experiment without setting up a project? Copy and paste this code into CodePen or JSFiddle to see Web Components in action immediately:

class ColorToggler extends HTMLElement {
  constructor() {
    super();
    this.colors = ['#e91e63', '#2196f3', '#4caf50', '#ff9800'];
    this.currentIndex = 0;
  }

  connectedCallback() {
    this.style.display = 'block';
    this.style.padding = '20px';
    this.style.margin = '20px';
    this.style.backgroundColor = this.colors[0];
    this.style.color = 'white';
    this.style.borderRadius = '8px';
    this.style.cursor = 'pointer';
    this.style.transition = 'background-color 0.3s';
    this.style.userSelect = 'none';

    this.innerHTML = '<h3>Click me to change color!</h3>';
    this.addEventListener('click', this.cycleColor.bind(this));
  }

  cycleColor() {
    this.currentIndex = (this.currentIndex + 1) % this.colors.length;
    this.style.backgroundColor = this.colors[this.currentIndex];
  }

  disconnectedCallback() {
    this.removeEventListener('click', this.cycleColor);
  }
}

customElements.define('color-toggler', ColorToggler);

// Add to your HTML: <color-toggler></color-toggler>

Challenge: Can you modify the component to display the current color name when clicked? (Solution at the bottom of this article)


Challenge Solution

class ColorToggler extends HTMLElement {
  constructor() {
    super();
    this.colors = [
      { hex: '#e91e63', name: 'Pink' },
      { hex: '#2196f3', name: 'Blue' },
      { hex: '#4caf50', name: 'Green' },
      { hex: '#ff9800', name: 'Orange' }
    ];
    this.currentIndex = 0;
  }

  connectedCallback() {
    this.style.display = 'block';
    this.style.padding = '20px';
    this.style.margin = '20px';
    this.style.backgroundColor = this.colors[0].hex;
    this.style.color = 'white';
    this.style.borderRadius = '8px';
    this.style.cursor = 'pointer';
    this.style.transition = 'background-color 0.3s';
    this.style.userSelect = 'none';    
    this.innerHTML = `<h3>Click me! (Current: ${this.colors[0].name})</h3>`;
    this.addEventListener('click', this.cycleColor.bind(this));
  }

  cycleColor() {
    this.currentIndex = (this.currentIndex + 1) % this.colors.length;
    const currentColor = this.colors[this.currentIndex];
    this.style.backgroundColor = currentColor.hex;
    this.innerHTML = `<h3>Click me! (Current: ${currentColor.name})</h3>`;
  }

      disconnectedCallback() {
    this.removeEventListener('click', this.cycleColor);
  }
}

customElements.define('color-toggler', ColorToggler);


Additional Resources:

0
Subscribe to my newsletter

Read articles from Mikey Nichols directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mikey Nichols
Mikey Nichols

I am an aspiring web developer on a mission to kick down the door into tech. Join me as I take the essential steps toward this goal and hopefully inspire others to do the same!