Component Communication & Custom Events
📖 Concept
Component communication is essential for building modular, reusable LWC components. There are four main patterns:
1. Parent → Child: @api Properties Parent passes data down to child via public properties.
2. Child → Parent: Custom Events Child dispatches events, parent listens with on-prefixed handlers.
3. Unrelated Components: Lightning Message Service (LMS) Publish/subscribe messaging across the DOM — components don't need a parent-child relationship.
4. Sibling Communication:
- Via shared parent (child A → event → parent → @api → child B)
- Via LMS (publish/subscribe)
- Via Platform Events (empApi for real-time)
Custom Events — the standard pattern:
// Child dispatches
this.dispatchEvent(new CustomEvent('itemselected', {
detail: { id: '001xxx', name: 'Acme' },
bubbles: false, // Only reaches immediate parent
composed: false // Doesn't cross Shadow DOM boundary
}));
// Parent listens (in template)
// <c-child-component onitemselected={handleSelection}></c-child-component>
Lightning Message Service (LMS): For communication between components that don't share a parent-child relationship. Works across Aura, LWC, and Visualforce.
Event bubbling considerations:
bubbles: false(default) — event only reaches the immediate parentbubbles: true— event bubbles up the DOM treecomposed: true— event crosses Shadow DOM boundaries- Use bubbles/composed cautiously — they create tight coupling
💻 Code Example
1// Component Communication Patterns23// === 1. PARENT TO CHILD — @api Properties ===45// Parent template6// <c-child-card account-name={selectedName} record-count={count}></c-child-card>78// childCard.js9import { LightningElement, api } from 'lwc';1011export default class ChildCard extends LightningElement {12 @api accountName; // Set by parent13 @api recordCount; // Set by parent1415 // Public method — parent can call this16 @api17 resetState() {18 this.internalState = 'default';19 }2021 // Note: @api properties are READ-ONLY inside the component22 // Copy to internal variable if you need to modify23 internalState = 'default';24}2526// Parent calling child method27// parentComponent.js28// const child = this.template.querySelector('c-child-card');29// child.resetState();3031// === 2. CHILD TO PARENT — Custom Events ===3233// contactList.js (Child)34import { LightningElement, api, wire } from 'lwc';35import getContacts from '@salesforce/apex/ContactController.getContacts';3637export default class ContactList extends LightningElement {38 @api accountId;39 contacts = [];4041 @wire(getContacts, { accountId: '$accountId' })42 wiredContacts({ data, error }) {43 if (data) this.contacts = data;44 }4546 handleSelect(event) {47 const contactId = event.currentTarget.dataset.id;48 const contact = this.contacts.find(c => c.Id === contactId);4950 // Dispatch custom event to parent51 this.dispatchEvent(new CustomEvent('contactselected', {52 detail: {53 contactId: contact.Id,54 contactName: contact.FirstName + ' ' + contact.LastName,55 email: contact.Email56 }57 }));58 }59}6061// accountPage.js (Parent)62// <c-contact-list63// account-id={recordId}64// oncontactselected={handleContactSelected}>65// </c-contact-list>6667// handleContactSelected(event) {68// const { contactId, contactName, email } = event.detail;69// this.selectedContact = { contactId, contactName, email };70// }7172// === 3. UNRELATED COMPONENTS — Lightning Message Service ===7374// First, create a Message Channel (metadata XML)75// messageChannels/Record_Selected.messageChannel-meta.xml76// <?xml version="1.0" encoding="UTF-8"?>77// <LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">78// <masterLabel>Record Selected</masterLabel>79// <isExposed>true</isExposed>80// <lightningMessageFields>81// <fieldName>recordId</fieldName>82// <description>The selected record ID</description>83// </lightningMessageFields>84// <lightningMessageFields>85// <fieldName>recordName</fieldName>86// </lightningMessageFields>87// </LightningMessageChannel>8889// Publisher component90import { LightningElement, wire } from 'lwc';91import { publish, MessageContext } from 'lightning/messageService';92import RECORD_SELECTED from '@salesforce/messageChannel/Record_Selected__c';9394export default class RecordPublisher extends LightningElement {95 @wire(MessageContext)96 messageContext;9798 handleRecordClick(event) {99 const payload = {100 recordId: event.currentTarget.dataset.id,101 recordName: event.currentTarget.dataset.name102 };103 publish(this.messageContext, RECORD_SELECTED, payload);104 }105}106107// Subscriber component (can be ANYWHERE on the page)108import { LightningElement, wire } from 'lwc';109import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';110import RECORD_SELECTED from '@salesforce/messageChannel/Record_Selected__c';111112export default class RecordSubscriber extends LightningElement {113 selectedRecordId;114 selectedRecordName;115 subscription = null;116117 @wire(MessageContext)118 messageContext;119120 connectedCallback() {121 this.subscription = subscribe(122 this.messageContext,123 RECORD_SELECTED,124 (message) => this.handleMessage(message)125 );126 }127128 disconnectedCallback() {129 unsubscribe(this.subscription);130 this.subscription = null;131 }132133 handleMessage(message) {134 this.selectedRecordId = message.recordId;135 this.selectedRecordName = message.recordName;136 }137}138139// === 4. REAL-TIME EVENTS — empApi (Platform Events in LWC) ===140import { LightningElement } from 'lwc';141import { subscribe as empSubscribe, unsubscribe as empUnsubscribe,142 onError } from 'lightning/empApi';143144export default class RealTimeNotifications extends LightningElement {145 channelName = '/event/Order_Event__e';146 subscription = {};147148 connectedCallback() {149 this.handleSubscribe();150 this.registerErrorListener();151 }152153 handleSubscribe() {154 empSubscribe(this.channelName, -1, (response) => {155 // Handle incoming Platform Event156 const eventData = response.data.payload;157 console.log('New order event:', eventData);158 // Update UI, show notification, etc.159 }).then(sub => {160 this.subscription = sub;161 });162 }163164 registerErrorListener() {165 onError((error) => {166 console.error('EMP API error:', error);167 });168 }169170 disconnectedCallback() {171 empUnsubscribe(this.subscription);172 }173}
🏋️ Practice Exercise
Component Communication Practice:
- Build a parent-child component pair using @api properties and custom events
- Create a sibling communication pattern through a shared parent
- Implement Lightning Message Service for two unrelated components on the same page
- Build a real-time notification component using empApi (Platform Events in LWC)
- Create a master-detail UI: selecting an account shows contacts, selecting a contact shows details
- Implement a pub/sub pattern for 3 components that react to the same event
- Build a component that exposes a public @api method for the parent to call
- Create a search component that publishes results via LMS to a map/chart component
- Handle complex event data (arrays of objects) in custom events
- Write Jest tests for component communication (mock events, verify handlers)
⚠️ Common Mistakes
Using bubbles:true and composed:true without understanding the implications — events crossing Shadow DOM boundaries create tight coupling and make debugging harder
Not unsubscribing from LMS or empApi in disconnectedCallback — causes memory leaks and ghost event handlers
Passing mutable objects in event.detail — the receiver can modify the original data. Use Object.freeze() or spread for immutability
Using querySelector to communicate between components — this is fragile. Use @api properties, events, or LMS instead
Forgetting that LMS requires a Message Channel metadata XML file — it must be deployed to the org before the components can use it
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Component Communication & Custom Events. Login to unlock this feature.