README
1 min read18 headings
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 |