SfSkills Domains Docs GitHub

Home / lwc / lwc-pubsub-patterns

lwc-pubsub-patterns

Legacy pubsub communication between LWCs on the same page (deprecated — prefer LMS). Triggers: pubsub LWC, c-pubsub, pageRef pubsub. NOT for modern cross-component messaging — use lwc/message-channel-patterns. NOT for parent-child events — use lwc/lwc-custom-event-patterns.

Open the skill folder on GitHub


LWC Pub/Sub Patterns

Lightning Web Components communicate naturally down the component tree (parent passes props to child) and up (child fires CustomEvent, parent listens). The harder case is sibling communication: two components on the same page that have no parent-child relationship.

Salesforce's modern answer is Lightning Message Service (LMS). Before LMS shipped, the community converged on a custom pubsub utility (a singleton event-bus pattern in JavaScript). Both patterns appear in real codebases. This skill maps the choice and covers the implementation discipline that prevents subscription leaks.

When LMS is the right tool

LMS is the right tool when:

LMS is NOT the right tool for:

The Message Channel definition

Message channels are metadata. Define a channel at force-app/main/default/messageChannels/MyChannel.messageChannel-meta.xml:

<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>My Channel</masterLabel>
    <isExposed>true</isExposed>
    <description>Sibling sync: search component to results component.</description>
    <lightningMessageFields>
        <fieldName>recordId</fieldName>
        <description>The record selected in the search component.</description>
    </lightningMessageFields>
    <lightningMessageFields>
        <fieldName>action</fieldName>
        <description>One of: 'select', 'clear'.</description>
    </lightningMessageFields>
</LightningMessageChannel>

isExposed = true means other namespaces can use it. Set to false for managed-package internal channels.

Publish / subscribe in LWC

import { LightningElement, wire } from 'lwc';
import {
    publish, subscribe, unsubscribe, MessageContext, APPLICATION_SCOPE
} from 'lightning/messageService';
import MY_CHANNEL from '@salesforce/messageChannel/MyChannel__c';

export default class SearchComponent extends LightningElement {
    @wire(MessageContext) messageContext;
    subscription = null;

    connectedCallback() {
        this.subscription = subscribe(
            this.messageContext,
            MY_CHANNEL,
            (msg) => this.handleMessage(msg),
            { scope: APPLICATION_SCOPE }
        );
    }

    disconnectedCallback() {
        unsubscribe(this.subscription);
        this.subscription = null;
    }

    publishSelection(recordId) {
        publish(this.messageContext, MY_CHANNEL, {
            recordId,
            action: 'select'
        });
    }

    handleMessage(msg) { /* ... */ }
}

Two things to get right:

  1. @wire(MessageContext) provides the message context the

    subscribe / publish calls require.

  2. Always unsubscribe in disconnectedCallback to prevent

    memory leaks across page navigations.

scope: APPLICATION vs scope: ACTIVE

ScopeReceives messages from
APPLICATION_SCOPEAll subscribers anywhere in the Lightning Experience app
ACTIVE (default)Only subscribers in the active Lightning navigation context (current tab in console UX, current app)

APPLICATION_SCOPE is broader; use it when the publisher and subscriber may live in different apps within the same Lightning session (e.g. utility bar component and main page). ACTIVE is the default and is what you usually want for pages that should only talk to themselves.

The legacy pubsub utility

Pre-LMS, a community-shared utility module called pubsub (often literally named that in source) provided a singleton event bus. Pattern:

import { fireEvent, registerListener, unregisterListener } from 'c/pubsub';

connectedCallback() {
    registerListener('mySearchEvent', this.handleEvent, this);
}
disconnectedCallback() {
    unregisterAllListeners(this);
}

It works but has limits:

Migration. Replace c/pubsub import with lightning/messageService. Replace the event name with a Message Channel. Most renames are mechanical.

  1. Confirm sibling communication is required. Parent-child should use props / CustomEvent; only reach for LMS when there is no shared parent.
  2. Confirm cross-tab is not required. LMS does not cross tabs. For cross-tab, use Platform Events.
  3. Define the Message Channel. Create MyChannel.messageChannel-meta.xml with named fields and a description. Treat the channel as a contract; named fields are the schema.
  4. Implement publish / subscribe. Use @wire(MessageContext); subscribe in connectedCallback; always unsubscribe in disconnectedCallback.
  5. Pick the scope. APPLICATION_SCOPE for cross-app within the Lightning session; default (ACTIVE) for same-page.
  6. Test the unsubscribe. Navigate away and back; the previous subscription should not fire on the second visit. Memory leaks here surface as duplicate handler invocations.
  7. For legacy pubsub users, migrate to LMS rather than extending the utility. Custom utility maintenance cost compounds.

What This Skill Does Not Cover

TopicSee instead
Parent-child LWC props / CustomEventlwc/lwc-component-communication
Cross-tab or cross-user eventsPlatform Events / integration/change-data-capture-patterns
Aura-to-LWC interop generallylwc/aura-lwc-interop-patterns
LWC reactivity (@track, @api, @wire)lwc/lwc-reactivity-patterns

Package files