LWC Event Handling: Parent-Child, Sibling, and Cross-DOM Communication
Event handling in LWC is where most developers get stuck. The framework gives you three patterns and choosing the wrong one creates spaghetti code. Child-to-parent: use CustomEvent. Parent-to-child: use public @api methods or properties. Sibling-to-sibling: use a shared service component or Lightning Message Service.
For child-to-parent, always create custom events with detail payloads. Name them in kebab-case (my-event) and dispatch them with this.dispatchEvent(). The parent listens with onmyevent (all lowercase, no hyphens, prefixed with "on"). This is the most common source of bugs: the event is named "my-event" but the handler must be "onmyevent", not "onmy-event" or "onMyEvent". The event bubbles by default only within the component shadow DOM. If you need it to cross shadow boundaries, set bubbles: true and composed: true.
For sibling communication, Lightning Message Service (LMS) is the correct approach. Create a message channel in your project metadata, publish from one component, subscribe from another. This works across Aura and LWC, across different parts of the page, and even across utility bar components. Avoid the temptation to use pubsub patterns from old blog posts — LMS replaced all of that.
Code Example
// CHILD-TO-PARENT: CustomEvent
// child.js
handleSelect() {
const event = new CustomEvent('itemselected', {
detail: { recordId: this.recordId, name: this.name }
// bubbles: true, composed: true ← only if crossing shadow DOM
});
this.dispatchEvent(event);
}
// parent.html
// <c-child onitemselected={handleItemSelected}></c-child>
// parent.js
handleItemSelected(event) {
const { recordId, name } = event.detail;
// Use the data
}
// PARENT-TO-CHILD: @api properties + methods
// child.js
export default class Child extends LightningElement {
@api recordId; // Property — set from parent template
@api // Method — called from parent JS
refresh() {
// Re-fetch data
}
}
// parent.js
this.template.querySelector('c-child').refresh();
// SIBLING: Lightning Message Service
// messageChannel/Record_Selected.messageChannel-meta.xml
import { publish, subscribe, MessageContext } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/Record_Selected__c';
// Publisher
@wire(MessageContext) messageContext;
handleClick() {
publish(this.messageContext, RECORD_SELECTED, {
recordId: this.selectedId
});
}
// Subscriber (different component)
@wire(MessageContext) messageContext;
connectedCallback() {
this.subscription = subscribe(
this.messageContext, RECORD_SELECTED,
(message) => { this.selectedId = message.recordId; }
);
}Need this implemented in your org?
I've shipped these patterns in production for 10+ years.
View Consulting →Enjoyed this? Get more like it.
Glen's Musings — AI, investing, and building things. Occasional. Free.
More LWC Tips
LWC: Wire Service vs Imperative Apex — When to Use Each
Lightning Web Components give you two ways to call Apex: the wire service and imperative calls. The wire service is reac...
Read moreAdvancedTesting LWC with Jest: The Patterns That Actually Work in CI/CD
Jest testing for LWC is powerful but the setup and patterns are different from standard JavaScript testing. The key insi...
Read more