Docs
25.4-Component-Patterns
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
| Pattern | Use Case |
|---|---|
| Event Bus | Cross-component communication |
| Reactive State | Automatic UI updates |
| Mixins | Shared functionality |
| Form Associated | Native form integration |
| Cleanup | Resource management |
| ARIA Support | Accessibility |
| Debounced Render | Performance |