When two components on the same Lightning page need to talk but don’t share a parent-child relationship — say a list on the left and a detail panel on the right — CustomEvent won’t reach across. That’s the job of Lightning Message Service (LMS).
What LMS is
LMS is a Salesforce-provided messaging API that lets components communicate across the DOM regardless of where they sit in the page hierarchy or which framework they’re built in. The three pieces:
- A Message Channel — a
.messageChannel-meta.xmlmetadata file declaring the channel and its fields. - The
lightning/messageServicemodule — JS APIs for publishing and subscribing. - The
@wire(MessageContext)adapter — gives the component its identity in the LMS system.
Setting up a channel
<!-- messageChannels/RecordSelected.messageChannel-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
<masterLabel>Record Selected</masterLabel>
<isExposed>true</isExposed>
<lightningMessageFields>
<fieldName>recordId</fieldName>
</lightningMessageFields>
</LightningMessageChannel>
Publishing a message
import { LightningElement, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/RecordSelected__c';
export default class ContactList extends LightningElement {
@wire(MessageContext) messageContext;
handleSelect(event) {
publish(this.messageContext, RECORD_SELECTED, {
recordId: event.detail.contactId
});
}
}
Subscribing to a message
import { LightningElement, wire } from 'lwc';
import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/RecordSelected__c';
export default class ContactDetail extends LightningElement {
@wire(MessageContext) messageContext;
subscription = null;
selectedId;
connectedCallback() {
this.subscription = subscribe(this.messageContext, RECORD_SELECTED,
(msg) => { this.selectedId = msg.recordId; });
}
disconnectedCallback() {
unsubscribe(this.subscription);
this.subscription = null;
}
}
The disconnectedCallback cleanup is not optional. Without it, the subscription survives the component and leaks memory across navigation.
LMS vs the old pub-sub library
Before LMS, the community used a pubsub.js helper from a Salesforce sample app. It worked, but it had three problems:
| pub-sub (legacy) | LMS (official) | |
|---|---|---|
| Officially supported by Salesforce | No | Yes |
| Works across LWC, Aura, and Visualforce | LWC only | All three |
| Survives navigation in console apps | No — singleton scoped to page | Yes |
| Subscriber registry | Static module — fragile | Per-context, garbage-collected |
| Strongly typed channels | No | Yes — schema declared in metadata |
| In-built scope control | No | Yes — APPLICATION vs ACTIVE scope |
If you see pubsub.js in a codebase today, treat it as tech debt. Anything new should use LMS.
LMS scopes
subscribe() accepts a third argument that controls how aggressively the listener fires:
subscribe(this.messageContext, CHANNEL, handler, { scope: APPLICATION_SCOPE });
APPLICATION_SCOPE— receive messages from anywhere in the app, including inactive console tabs.- Default scope — receive only from the same active page/tab.
In a console app, APPLICATION_SCOPE lets a utility-bar component pick up a message from any open subtab. Without it, the message is scoped to the tab it was published from.
When to use which messaging tool
| Scenario | Tool |
|---|---|
| Child to direct parent | CustomEvent |
| Sibling on the same page, different DOM branches | LMS |
| Parent to child | @api properties |
| Cross-framework (LWC ↔ Aura ↔ Visualforce) | LMS |
| Page-wide global state | LMS with APPLICATION_SCOPE |
| Server-pushed events from Apex | Platform Events + lightning/empApi (Streaming API) |
Don’t use LMS as a shortcut to avoid passing props down a small tree — events that could be CustomEvents are easier to debug than channel messages.
Interview signal
The interviewer wants to hear that you understand LMS replaced pub-sub because pub-sub couldn’t cross frameworks or survive console navigation. If you can explain MessageContext, the schema-driven channel, and the cleanup pattern in disconnectedCallback, you’ve covered everything that matters.
Verified against: LWC Developer Guide — Communicate Across the DOM with Lightning Message Service. Last reviewed 2026-05-17 for Spring ‘26 release.