Read the screenplay: FANNIEGATE — $7 trillion. 17 years. The biggest fraud in American capital markets.
⚛️ LWCAdvanced2026-03-04

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