# Lightning Message Service (LMS) Guide
Complete guide to cross-DOM component communication using Lightning Message Service.
---
## Table of Contents
1. [Overview](#overview)
2. [When to Use LMS](#when-to-use-lms)
3. [Message Channel Setup](#message-channel-setup)
4. [Publishing Messages](#publishing-messages)
5. [Subscribing to Messages](#subscribing-to-messages)
6. [Scopes](#scopes)
7. [Advanced Patterns](#advanced-patterns)
8. [Best Practices](#best-practices)
9. [Troubleshooting](#troubleshooting)
---
## Overview
Lightning Message Service (LMS) enables communication between components across different DOM contexts:
- Lightning Web Components (LWC)
- Aura Components
- Visualforce pages (in Lightning Experience)
**Key Benefits**:
- Cross-DOM communication (Shadow DOM boundaries)
- Loosely coupled components
- Publish-subscribe pattern
- Type-safe messaging with message channels
---
## When to Use LMS
| Use Case | Recommended Pattern |
|----------|---------------------|
| Parent → Child | `@api` properties (simple, direct) |
| Child → Parent | Custom Events (simple, direct) |
| Sibling → Sibling (same hierarchy) | Parent mediator + Custom Events |
| **Cross-DOM communication** | **Lightning Message Service** |
| **App Builder page components** | **Lightning Message Service** |
| **Aura ↔ LWC communication** | **Lightning Message Service** |
| **Visualforce ↔ LWC (in LEX)** | **Lightning Message Service** |
**Rule of Thumb**: Use LMS when components cannot directly reference each other or cross DOM boundaries.
---
## Message Channel Setup
### 1. Create Message Channel Metadata
Lightning Message Channels are metadata files that define the message schema.
**File**: `force-app/main/default/messageChannels/AccountSelected__c.messageChannel-meta.xml`
```xml
Message channel for account selection events
true
Account ID
accountId
Account Name
accountName
Source component identifier
source
Account Selected
```
### 2. Deploy Message Channel
```bash
sf project deploy start -m LightningMessageChannel:AccountSelected__c
```
### 3. Import Message Channel in Component
```javascript
import ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';
```
---
## Publishing Messages
### Basic Publisher Pattern
```javascript
// accountPublisher.js
import { LightningElement, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';
export default class AccountPublisher extends LightningElement {
@wire(MessageContext)
messageContext;
handleAccountClick(event) {
const accountId = event.target.dataset.id;
const accountName = event.target.dataset.name;
// Create payload
const payload = {
accountId: accountId,
accountName: accountName,
source: 'accountPublisher'
};
// Publish message
publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, payload);
}
}
```
```html
```
### Publisher with Conditional Logic
```javascript
handlePublish(accountData) {
// Validate before publishing
if (!this.messageContext) {
console.error('MessageContext not initialized');
return;
}
if (!accountData || !accountData.Id) {
console.error('Invalid account data');
return;
}
const payload = {
accountId: accountData.Id,
accountName: accountData.Name,
source: this.componentName,
timestamp: Date.now()
};
publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, payload);
// Optional: Show toast confirmation
this.dispatchEvent(new ShowToastEvent({
title: 'Selection Published',
message: `Account "${accountData.Name}" selected`,
variant: 'success'
}));
}
```
---
## Subscribing to Messages
### Basic Subscriber Pattern
```javascript
// accountSubscriber.js
import { LightningElement, wire } from 'lwc';
import { subscribe, unsubscribe, MessageContext, APPLICATION_SCOPE } from 'lightning/messageService';
import ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';
export default class AccountSubscriber extends LightningElement {
subscription = null;
selectedAccountId;
selectedAccountName;
@wire(MessageContext)
messageContext;
connectedCallback() {
this.subscribeToChannel();
}
disconnectedCallback() {
this.unsubscribeFromChannel();
}
subscribeToChannel() {
if (!this.subscription) {
this.subscription = subscribe(
this.messageContext,
ACCOUNT_SELECTED_CHANNEL,
(message) => this.handleMessage(message),
{ scope: APPLICATION_SCOPE }
);
}
}
unsubscribeFromChannel() {
unsubscribe(this.subscription);
this.subscription = null;
}
handleMessage(message) {
this.selectedAccountId = message.accountId;
this.selectedAccountName = message.accountName;
console.log('Message received from:', message.source);
console.log('Account ID:', message.accountId);
}
}
```
```html
- Account ID:
- {selectedAccountId}
- Account Name:
- {selectedAccountName}
No account selected
```
### Subscriber with Filtering
```javascript
handleMessage(message) {
// Ignore messages from this component (avoid self-updates)
if (message.source === this.componentName) {
return;
}
// Filter by specific conditions
if (message.accountId && message.accountId.startsWith('001')) {
this.selectedAccountId = message.accountId;
this.selectedAccountName = message.accountName;
// Fetch additional data if needed
this.loadAccountDetails(message.accountId);
}
}
async loadAccountDetails(accountId) {
try {
const data = await getAccountDetails({ accountId });
this.accountDetails = data;
} catch (error) {
this.handleError(error);
}
}
```
---
## Scopes
LMS supports two subscription scopes:
| Scope | Behavior | Use Case |
|-------|----------|----------|
| `APPLICATION_SCOPE` | Receive messages from entire app | Cross-page communication, global state |
| `undefined` (default) | Receive messages only within active tab | Tab-specific communication |
### Application Scope Example
```javascript
import { APPLICATION_SCOPE } from 'lightning/messageService';
subscribe(
this.messageContext,
ACCOUNT_SELECTED_CHANNEL,
(message) => this.handleMessage(message),
{ scope: APPLICATION_SCOPE }
);
```
### Tab Scope Example
```javascript
// No scope specified = tab scope only
subscribe(
this.messageContext,
ACCOUNT_SELECTED_CHANNEL,
(message) => this.handleMessage(message)
);
```
---
## Advanced Patterns
### 1. Multiple Subscriptions
```javascript
export default class MultiSubscriber extends LightningElement {
accountSubscription = null;
contactSubscription = null;
@wire(MessageContext) messageContext;
connectedCallback() {
// Subscribe to account channel
this.accountSubscription = subscribe(
this.messageContext,
ACCOUNT_SELECTED_CHANNEL,
(message) => this.handleAccountMessage(message),
{ scope: APPLICATION_SCOPE }
);
// Subscribe to contact channel
this.contactSubscription = subscribe(
this.messageContext,
CONTACT_SELECTED_CHANNEL,
(message) => this.handleContactMessage(message),
{ scope: APPLICATION_SCOPE }
);
}
disconnectedCallback() {
unsubscribe(this.accountSubscription);
unsubscribe(this.contactSubscription);
this.accountSubscription = null;
this.contactSubscription = null;
}
handleAccountMessage(message) {
// Handle account-specific logic
}
handleContactMessage(message) {
// Handle contact-specific logic
}
}
```
### 2. Publish-Subscribe in Same Component
```javascript
export default class PublisherSubscriber extends LightningElement {
subscription = null;
@wire(MessageContext) messageContext;
connectedCallback() {
// Subscribe to messages from OTHER components
this.subscription = subscribe(
this.messageContext,
ACCOUNT_SELECTED_CHANNEL,
(message) => this.handleMessage(message),
{ scope: APPLICATION_SCOPE }
);
}
disconnectedCallback() {
unsubscribe(this.subscription);
}
handleMessage(message) {
// Filter out own messages
if (message.source === 'myComponent') {
return;
}
// Process external messages
this.selectedAccountId = message.accountId;
}
handleLocalSelection(event) {
const accountId = event.detail.id;
// Publish for other components
publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, {
accountId,
source: 'myComponent'
});
// Update own state directly (don't rely on subscription)
this.selectedAccountId = accountId;
}
}
```
### 3. Conditional Subscription
```javascript
export default class ConditionalSubscriber extends LightningElement {
@api enableLiveUpdates = false;
subscription = null;
@wire(MessageContext) messageContext;
connectedCallback() {
if (this.enableLiveUpdates) {
this.subscribeToChannel();
}
}
@api
toggleLiveUpdates(enabled) {
this.enableLiveUpdates = enabled;
if (enabled) {
this.subscribeToChannel();
} else {
this.unsubscribeFromChannel();
}
}
subscribeToChannel() {
if (!this.subscription) {
this.subscription = subscribe(
this.messageContext,
ACCOUNT_SELECTED_CHANNEL,
(message) => this.handleMessage(message),
{ scope: APPLICATION_SCOPE }
);
}
}
unsubscribeFromChannel() {
if (this.subscription) {
unsubscribe(this.subscription);
this.subscription = null;
}
}
}
```
### 4. Message Buffering
```javascript
export default class MessageBuffer extends LightningElement {
messageQueue = [];
isProcessing = false;
handleMessage(message) {
this.messageQueue.push(message);
this.processQueue();
}
async processQueue() {
if (this.isProcessing || this.messageQueue.length === 0) {
return;
}
this.isProcessing = true;
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
await this.processMessage(message);
}
this.isProcessing = false;
}
async processMessage(message) {
// Simulate async processing
return new Promise(resolve => {
setTimeout(() => {
this.selectedAccountId = message.accountId;
resolve();
}, 100);
});
}
}
```
---
## Best Practices
### 1. Always Unsubscribe
```javascript
disconnectedCallback() {
// CRITICAL: Prevent memory leaks
if (this.subscription) {
unsubscribe(this.subscription);
this.subscription = null;
}
}
```
### 2. Validate MessageContext
```javascript
handlePublish(data) {
if (!this.messageContext) {
console.warn('MessageContext not available');
return;
}
publish(this.messageContext, CHANNEL, data);
}
```
### 3. Use Descriptive Payloads
```javascript
// BAD - Unclear payload
publish(this.messageContext, CHANNEL, { id: '001xxx' });
// GOOD - Clear, descriptive payload
publish(this.messageContext, CHANNEL, {
accountId: '001xxx000003DGQ',
accountName: 'Acme Corp',
source: 'accountList',
timestamp: Date.now(),
metadata: {
action: 'selected',
view: 'list'
}
});
```
### 4. Document Message Channels
```javascript
/**
* Publishes account selection event to AccountSelected__c channel
* @param {Object} payload
* @param {String} payload.accountId - Salesforce Account ID
* @param {String} payload.accountName - Account Name
* @param {String} payload.source - Component identifier
*/
publishAccountSelection(payload) {
publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, payload);
}
```
### 5. Error Handling
```javascript
handleMessage(message) {
try {
if (!message || !message.accountId) {
throw new Error('Invalid message payload');
}
this.selectedAccountId = message.accountId;
this.loadAccountDetails(message.accountId);
} catch (error) {
console.error('Error processing message:', error);
this.dispatchEvent(new ShowToastEvent({
title: 'Error',
message: 'Failed to process message',
variant: 'error'
}));
}
}
```
---
## Troubleshooting
### Issue: Messages Not Received
**Checklist**:
1. Is `MessageContext` wired correctly?
```javascript
@wire(MessageContext) messageContext;
```
2. Is subscription active?
```javascript
console.log('Subscription:', this.subscription); // Should not be null
```
3. Is the message channel deployed?
```bash
sf project deploy start -m LightningMessageChannel
```
4. Are publisher and subscriber using the same channel?
```javascript
// Both should import the same channel
import CHANNEL from '@salesforce/messageChannel/AccountSelected__c';
```
5. Is the scope correct?
```javascript
// For cross-page: APPLICATION_SCOPE
// For same page: no scope (default)
```
### Issue: Memory Leaks
**Cause**: Not unsubscribing in `disconnectedCallback()`
**Fix**:
```javascript
disconnectedCallback() {
unsubscribe(this.subscription);
this.subscription = null;
}
```
### Issue: Self-Updates
**Cause**: Component receives its own published messages
**Fix**: Filter by source
```javascript
handleMessage(message) {
if (message.source === this.componentName) {
return; // Ignore own messages
}
// Process message
}
```
---
## Testing LMS Components
### Mock MessageContext
```javascript
// testUtils.js
export const createMessageContextMock = () => {
return jest.fn();
};
export const mockPublish = jest.fn();
export const mockSubscribe = jest.fn();
export const mockUnsubscribe = jest.fn();
jest.mock('lightning/messageService', () => ({
publish: mockPublish,
subscribe: mockSubscribe,
unsubscribe: mockUnsubscribe,
MessageContext: Symbol('MessageContext'),
APPLICATION_SCOPE: Symbol('APPLICATION_SCOPE')
}), { virtual: true });
```
### Test Publisher
```javascript
import { createElement } from 'lwc';
import AccountPublisher from 'c/accountPublisher';
import { publish } from 'lightning/messageService';
import ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';
jest.mock('lightning/messageService');
describe('c-account-publisher', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('publishes account selection', () => {
const element = createElement('c-account-publisher', {
is: AccountPublisher
});
document.body.appendChild(element);
// Trigger selection
const accountCard = element.shadowRoot.querySelector('[data-id="001xxx"]');
accountCard.click();
// Assert publish was called
expect(publish).toHaveBeenCalledWith(
expect.anything(),
ACCOUNT_SELECTED_CHANNEL,
expect.objectContaining({
accountId: '001xxx'
})
);
});
});
```
### Test Subscriber
```javascript
import { createElement } from 'lwc';
import AccountSubscriber from 'c/accountSubscriber';
import { subscribe } from 'lightning/messageService';
jest.mock('lightning/messageService');
describe('c-account-subscriber', () => {
let messageHandler;
beforeEach(() => {
subscribe.mockImplementation((context, channel, handler, options) => {
messageHandler = handler;
return { subscription: 'mock-subscription' };
});
});
it('subscribes on connected', () => {
const element = createElement('c-account-subscriber', {
is: AccountSubscriber
});
document.body.appendChild(element);
expect(subscribe).toHaveBeenCalled();
});
it('handles incoming message', async () => {
const element = createElement('c-account-subscriber', {
is: AccountSubscriber
});
document.body.appendChild(element);
// Simulate message
messageHandler({
accountId: '001xxx',
accountName: 'Acme Corp'
});
await Promise.resolve();
const accountName = element.shadowRoot.querySelector('.account-name');
expect(accountName.textContent).toBe('Acme Corp');
});
});
```
---
## Complete Example: Account-Contact Sync
### Message Channel
```xml
Contact selection messaging
true
contactId
contactName
accountId
Contact Selected
```
### Publisher Component
```javascript
// contactList.js
import { LightningElement, api, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import CONTACT_SELECTED from '@salesforce/messageChannel/ContactSelected__c';
import getContacts from '@salesforce/apex/ContactController.getContacts';
export default class ContactList extends LightningElement {
@api accountId;
contacts;
@wire(MessageContext) messageContext;
@wire(getContacts, { accountId: '$accountId' })
wiredContacts({ data, error }) {
if (data) {
this.contacts = data;
}
}
handleContactSelect(event) {
const contactId = event.currentTarget.dataset.id;
const contact = this.contacts.find(c => c.Id === contactId);
publish(this.messageContext, CONTACT_SELECTED, {
contactId: contact.Id,
contactName: contact.Name,
accountId: this.accountId
});
}
}
```
### Subscriber Component
```javascript
// contactDetails.js
import { LightningElement, wire } from 'lwc';
import { subscribe, MessageContext, APPLICATION_SCOPE } from 'lightning/messageService';
import CONTACT_SELECTED from '@salesforce/messageChannel/ContactSelected__c';
import getContactDetails from '@salesforce/apex/ContactController.getContactDetails';
export default class ContactDetails extends LightningElement {
subscription = null;
contactId;
contactDetails;
@wire(MessageContext) messageContext;
connectedCallback() {
this.subscription = subscribe(
this.messageContext,
CONTACT_SELECTED,
(message) => this.handleContactSelected(message),
{ scope: APPLICATION_SCOPE }
);
}
disconnectedCallback() {
unsubscribe(this.subscription);
}
async handleContactSelected(message) {
this.contactId = message.contactId;
try {
this.contactDetails = await getContactDetails({
contactId: message.contactId
});
} catch (error) {
console.error('Error loading contact details:', error);
}
}
}
```
---
## Related Resources
- [component-patterns.md](component-patterns.md) - Parent-child communication
- [jest-testing.md](jest-testing.md) - Testing LMS components
- [Official LMS Documentation](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_message_channel)