Docs

README

25.4 Component Patterns

Overview

This section covers advanced patterns and best practices for building Web Components. Learn how to create maintainable, reusable, and performant component libraries.

Learning Objectives

  • Implement common component design patterns
  • Build component communication systems
  • Create state management for components
  • Handle component lifecycle effectively
  • Apply accessibility best practices
  • Test and debug Web Components

Component Communication Patterns

Parent to Child (Props/Attributes)

class ParentComponent extends HTMLElement {
  connectedCallback() {
    const child = this.querySelector('child-component');

    // Via attribute
    child.setAttribute('message', 'Hello');

    // Via property
    child.data = { name: 'John', age: 30 };
  }
}

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

  set data(value) {
    this._data = value;
    this.render();
  }
}

Child to Parent (Events)

class ChildComponent extends HTMLElement {
  handleClick() {
    this.dispatchEvent(
      new CustomEvent('item-selected', {
        detail: { id: this.itemId },
        bubbles: true,
        composed: true,
      })
    );
  }
}

class ParentComponent extends HTMLElement {
  connectedCallback() {
    this.addEventListener('item-selected', (e) => {
      console.log('Selected:', e.detail.id);
    });
  }
}

Sibling Communication (Event Bus)

class EventBus {
  static _events = new Map();

  static emit(event, data) {
    const handlers = this._events.get(event) || [];
    handlers.forEach((handler) => handler(data));
  }

  static on(event, handler) {
    if (!this._events.has(event)) {
      this._events.set(event, []);
    }
    this._events.get(event).push(handler);

    // Return unsubscribe function
    return () => {
      const handlers = this._events.get(event);
      const index = handlers.indexOf(handler);
      if (index > -1) handlers.splice(index, 1);
    };
  }
}

State Management Patterns

Local State

class StatefulComponent extends HTMLElement {
  constructor() {
    super();
    this._state = {};
  }

  get state() {
    return this._state;
  }

  setState(newState) {
    const oldState = { ...this._state };
    this._state = { ...this._state, ...newState };
    this.stateChanged(oldState, this._state);
    this.render();
  }

  stateChanged(oldState, newState) {
    // Override for side effects
  }
}

Reactive State with Proxy

function createReactiveState(initialState, onChange) {
  return new Proxy(initialState, {
    set(target, property, value) {
      const oldValue = target[property];
      target[property] = value;
      if (oldValue !== value) {
        onChange(property, value, oldValue);
      }
      return true;
    },
  });
}

class ReactiveComponent extends HTMLElement {
  constructor() {
    super();
    this.state = createReactiveState({}, (prop, value) => {
      this.render();
    });
  }
}

Global State Store

class Store {
  static _state = {};
  static _subscribers = [];

  static getState() {
    return this._state;
  }

  static setState(newState) {
    this._state = { ...this._state, ...newState };
    this._notify();
  }

  static subscribe(callback) {
    this._subscribers.push(callback);
    return () => {
      const index = this._subscribers.indexOf(callback);
      if (index > -1) this._subscribers.splice(index, 1);
    };
  }

  static _notify() {
    this._subscribers.forEach((cb) => cb(this._state));
  }
}

Lifecycle Patterns

Lazy Initialization

class LazyComponent extends HTMLElement {
  constructor() {
    super();
    this._initialized = false;
  }

  connectedCallback() {
    if (!this._initialized) {
      this._initialize();
      this._initialized = true;
    }
    this._activate();
  }

  disconnectedCallback() {
    this._deactivate();
  }

  _initialize() {
    // One-time setup (attach shadow, create templates)
  }

  _activate() {
    // Activate (add listeners, start timers)
  }

  _deactivate() {
    // Deactivate (remove listeners, stop timers)
  }
}

Cleanup Pattern

class CleanupComponent extends HTMLElement {
  constructor() {
    super();
    this._cleanupFunctions = [];
  }

  addCleanup(fn) {
    this._cleanupFunctions.push(fn);
  }

  connectedCallback() {
    // Add event listener with cleanup
    const handler = () => console.log('clicked');
    document.addEventListener('click', handler);
    this.addCleanup(() => document.removeEventListener('click', handler));

    // Add interval with cleanup
    const interval = setInterval(() => {}, 1000);
    this.addCleanup(() => clearInterval(interval));
  }

  disconnectedCallback() {
    this._cleanupFunctions.forEach((fn) => fn());
    this._cleanupFunctions = [];
  }
}

Composition Patterns

Mixin Pattern

const LoggerMixin = (Base) =>
  class extends Base {
    log(message) {
      console.log(`[${this.tagName}] ${message}`);
    }
  };

const EventMixin = (Base) =>
  class extends Base {
    emit(eventName, detail = {}) {
      this.dispatchEvent(
        new CustomEvent(eventName, {
          detail,
          bubbles: true,
          composed: true,
        })
      );
    }
  };

class MyComponent extends EventMixin(LoggerMixin(HTMLElement)) {
  connectedCallback() {
    this.log('Connected');
    this.emit('connected');
  }
}

Render Props / Function as Child

class DataProvider extends HTMLElement {
  async connectedCallback() {
    const data = await this.fetchData();

    // Call render function from attribute
    const renderFn = this.getAttribute('render');
    if (renderFn && window[renderFn]) {
      this.innerHTML = window[renderFn](data);
    }
  }

  async fetchData() {
    const url = this.getAttribute('url');
    const response = await fetch(url);
    return response.json();
  }
}

Form Integration Patterns

Form-Associated Component

class FormInput extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this._internals = this.attachInternals();
    this.attachShadow({ mode: 'open' });
  }

  get value() {
    return this._value;
  }

  set value(v) {
    this._value = v;
    this._internals.setFormValue(v);
  }

  get validity() {
    return this._internals.validity;
  }

  formResetCallback() {
    this.value = '';
  }

  formStateRestoreCallback(state) {
    this.value = state;
  }
}

Accessibility Patterns

ARIA Support

class AccessibleButton extends HTMLElement {
  static get observedAttributes() {
    return ['disabled', 'pressed'];
  }

  connectedCallback() {
    this.setAttribute('role', 'button');
    this.setAttribute('tabindex', '0');

    this.addEventListener('click', this.handleClick);
    this.addEventListener('keydown', this.handleKeydown);
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'disabled') {
      this.setAttribute('aria-disabled', newVal !== null);
      this.setAttribute('tabindex', newVal !== null ? '-1' : '0');
    }
    if (name === 'pressed') {
      this.setAttribute('aria-pressed', newVal !== null);
    }
  }

  handleKeydown = (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      this.click();
    }
  };
}

Performance Patterns

Debounced Rendering

class DebouncedComponent extends HTMLElement {
  constructor() {
    super();
    this._renderTimeout = null;
  }

  scheduleRender() {
    if (this._renderTimeout) {
      cancelAnimationFrame(this._renderTimeout);
    }
    this._renderTimeout = requestAnimationFrame(() => {
      this.render();
    });
  }
}

Virtual Scrolling

class VirtualList extends HTMLElement {
  // Render only visible items
  // Use IntersectionObserver for visibility
  // Recycle DOM nodes
}

Testing Patterns

// Component testing utilities
async function waitForComponent(tagName) {
  await customElements.whenDefined(tagName);
}

function createComponent(tagName, attributes = {}) {
  const element = document.createElement(tagName);
  Object.entries(attributes).forEach(([key, value]) => {
    element.setAttribute(key, value);
  });
  document.body.appendChild(element);
  return element;
}

function cleanupComponent(element) {
  if (element.parentNode) {
    element.parentNode.removeChild(element);
  }
}

Summary

PatternUse Case
Event BusCross-component communication
Reactive StateAutomatic UI updates
MixinsShared functionality
Form AssociatedNative form integration
CleanupResource management
ARIA SupportAccessibility
Debounced RenderPerformance

Resources

README - JavaScript Tutorial | DeepML