# 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 ``` ### 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)