# Lightning Message Service (LMS) Guide

> Complete guide to cross-DOM component communication using Lightning Message Service.

- Skill: `tools-only/lightning-message-service-lms-guide-2` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/lightning-message-service-lms-guide-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/lightning-message-service-lms-guide-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/lightning-message-service-lms-guide-2

---

# 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
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>Message channel for account selection events</description>
    <isExposed>true</isExposed>
    <lightningMessageFields>
        <description>Account ID</description>
        <fieldName>accountId</fieldName>
    </lightningMessageFields>
    <lightningMessageFields>
        <description>Account Name</description>
        <fieldName>accountName</fieldName>
    </lightningMessageFields>
    <lightningMessageFields>
        <description>Source component identifier</description>
        <fieldName>source</fieldName>
    </lightningMessageFields>
    <masterLabel>Account Selected</masterLabel>
</LightningMessageChannel>
```

### 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
<!-- accountPublisher.html -->
<template>
    <div class="slds-card">
        <div class="slds-card__header">
            <h2 class="slds-text-heading_medium">Account List</h2>
        </div>
        <div class="slds-card__body">
            <template for:each={accounts} for:item="account">
                <div key={account.Id}
                     class="slds-box slds-m-around_small"
                     data-id={account.Id}
                     data-name={account.Name}
                     onclick={handleAccountClick}>
                    {account.Name}
                </div>
            </template>
        </div>
    </div>
</template>
```

### 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
<!-- accountSubscriber.html -->
<template>
    <div class="slds-card">
        <div class="slds-card__header">
            <h2 class="slds-text-heading_medium">Selected Account</h2>
        </div>
        <div class="slds-card__body">
            <template lwc:if={selectedAccountId}>
                <dl class="slds-dl_horizontal">
                    <dt class="slds-dl_horizontal__label">Account ID:</dt>
                    <dd class="slds-dl_horizontal__detail">{selectedAccountId}</dd>
                    <dt class="slds-dl_horizontal__label">Account Name:</dt>
                    <dd class="slds-dl_horizontal__detail">{selectedAccountName}</dd>
                </dl>
            </template>
            <template lwc:else>
                <p class="slds-text-color_weak">No account selected</p>
            </template>
        </div>
    </div>
</template>
```

### 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
<!-- ContactSelected__c.messageChannel-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>Contact selection messaging</description>
    <isExposed>true</isExposed>
    <lightningMessageFields>
        <fieldName>contactId</fieldName>
    </lightningMessageFields>
    <lightningMessageFields>
        <fieldName>contactName</fieldName>
    </lightningMessageFields>
    <lightningMessageFields>
        <fieldName>accountId</fieldName>
    </lightningMessageFields>
    <masterLabel>Contact Selected</masterLabel>
</LightningMessageChannel>
```

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

