Skip to main content

Mastering Salesforce Lightning Message Service (LMS): The Pub-Sub Pattern in LWC

Updated Add a comment
💬 In plain words: Lightning Message Service (LMS) is Salesforce's official "Publish-Subscribe" (Pub-Sub) framework. It acts like a radio station inside a Salesforce page. One component acts as the DJ, broadcasting a message, and any other component on the page can tune into that station to hear it. This allows unrelated components (even across LWC, Aura, and Visualforce) to talk to each other without needing a messy parent-child relationship.

Before LMS, if you wanted a component in the left sidebar to update a component in the main page area, developers had to rely on complex URL hacks, window events, or unsupported custom Pub-Sub libraries. Lightning Message Service completely solved this by providing a secure, standardized, and metadata-driven messaging architecture built natively into the Salesforce platform.

📌 Key Points

  • Decoupled Architecture: Publishers don't need to know who is listening, and subscribers don't need to know who is sending.
  • Cross-Framework: A single message channel can connect Lightning Web Components (LWC), Aura components, and legacy Visualforce pages simultaneously.
  • Metadata-Backed: Channels are not just JavaScript files; they are official Salesforce metadata artifacts ending in .messageChannel-meta.xml.
  • Scope Control: You can broadcast messages to the entire application (across tabs) using APPLICATION_SCOPE, or restrict it to the active component tree.
🧭 360 Architecture Card — LMS Components
  • Message Channel: The XML definition of the "radio station."
  • Publisher: The component using the publish() method.
  • Subscriber: The component using the subscribe() method.
  • Message Context: The @wire(MessageContext) adapter that links the component's lifecycle to the LMS framework.

📡 Step 1: Create the Message Channel Metadata

Unlike standard custom events, LMS requires you to define a channel at the org level using XML. This makes the channel reusable and discoverable across your entire Salesforce org.

File Path: force-app/main/default/messageChannels/RecordUpdateChannel.messageChannel-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>RecordUpdateChannel</masterLabel>
    <isExposed>true</isExposed>
    <description>Broadcasts selected record IDs across unrelated components.</description>
    <lightningMessageFields>
        <fieldName>recordId</fieldName>
        <description>The 18-character Salesforce Record ID.</description>
    </lightningMessageFields>
    <lightningMessageFields>
        <fieldName>recordData</fieldName>
        <description>Additional payload string for context.</description>
    </lightningMessageFields>
</LightningMessageChannel>
⚠ DEVELOPER TRAP: The __c Suffix
Even though this is not a Custom Object, you must append __c to the end of your channel name when importing it into your JavaScript file (e.g., RecordUpdateChannel__c). Forgetting this will cause a compilation error!

📤 Step 2: Build the Publisher Component

The publisher component imports the channel and uses the publish() method to send a JSON payload into the ether.

// lmsPublisher.js
import { LightningElement, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import RECORD_UPDATE_CHANNEL from '@salesforce/messageChannel/RecordUpdateChannel__c';

export default class LmsPublisher extends LightningElement {
    
    // Wires the component to the LMS framework
    @wire(MessageContext)
    messageContext;

    handlePublishClick() {
        // Construct the payload matching your XML fields
        const payload = { 
            recordId: '0018W00002E3XYZ', 
            recordData: 'High Priority Account' 
        };
        
        // Broadcast the message
        publish(this.messageContext, RECORD_UPDATE_CHANNEL, payload);
    }
}

📥 Step 3: Build the Subscriber Component

The subscriber listens for messages on the channel. It is crucial to handle subscriptions properly so they are active when the component is on screen, and safely destroyed when the user navigates away.

// lmsSubscriber.js
import { LightningElement, wire } from 'lwc';
import { subscribe, unsubscribe, APPLICATION_SCOPE, MessageContext } from 'lightning/messageService';
import RECORD_UPDATE_CHANNEL from '@salesforce/messageChannel/RecordUpdateChannel__c';

export default class LmsSubscriber extends LightningElement {
    receivedRecordId = 'Waiting for broadcast...';
    subscription = null;

    @wire(MessageContext)
    messageContext;

    // Subscribe when the component is inserted into the DOM
    connectedCallback() {
        this.subscribeToChannel();
    }

    // Safely unsubscribe when the component is removed
    disconnectedCallback() {
        this.unsubscribeFromChannel();
    }

    subscribeToChannel() {
        // Prevent duplicate subscriptions
        if (!this.subscription) {
            this.subscription = subscribe(
                this.messageContext,
                RECORD_UPDATE_CHANNEL,
                (message) => this.handleMessage(message),
                { scope: APPLICATION_SCOPE }
            );
        }
    }

    unsubscribeFromChannel() {
        if (this.subscription) {
            unsubscribe(this.subscription);
            this.subscription = null;
        }
    }

    handleMessage(message) {
        this.receivedRecordId = `Received: ${message.recordId} - ${message.recordData}`;
    }
}

🚨 4. Common Traps & Best Practices

⚠ MEMORY LEAK TRAP: Failing to Unsubscribe
Technically, if you use @wire(MessageContext), Salesforce automatically cleans up the subscription when the component is destroyed. However, relying purely on auto-teardown is risky if you ever conditionally pause subscriptions or switch to createMessageContext(). Best Practice: Always explicitly call unsubscribe(this.subscription) inside your disconnectedCallback() to guarantee you never create memory leaks or ghost event listeners.
🧠 CORE ARCHITECTURAL RULE: Only use LMS when components are completely unrelated. If you have a strict Parent-to-Child component hierarchy, use standard DOM Custom Events (moving up) or `@api` public properties (moving down). LMS is too heavy for simple parent-child data passing.
  • Use APPLICATION_SCOPE wisely: Passing { scope: APPLICATION_SCOPE } into your subscribe() method allows your component to hear messages across different Lightning Console sub-tabs or standard browser tabs. If omitted, it defaults to the active navigation context only.
  • Keep Payloads Lightweight: Do not pass massive arrays of queried data through LMS. Pass a recordId and a status flag, and let the receiving component query the fresh data itself using Lightning Data Service (LDS).

❓ Core Interview Q&A

Q: What is the difference between Lightning Message Service (LMS) and Platform Events?

🎯 Say this: Lightning Message Service operates entirely on the client-side (browser UI). It is designed purely for UI components on the same page to talk to each other instantly without hitting the server. Platform Events operate on the server-side (backend). You use Platform Events when you need to send messages from Salesforce out to an external system, or when an external system needs to trigger an automation inside Salesforce.

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.