LWC Zero to Hero Module 7 — Pub-Sub & Lightning Message Service
📻 LWC Zero to Hero — Module 7 of 15
Pub-Sub & Lightning Message Service
Module 6 solved parent-child communication. But what about two sibling components with no relationship at all? This module solves that — and even crosses into Aura and Visualforce.
Module 7 of 15 (counting Module 0 as a bonus prerequisite) · Phase 3: Component Communication
🎯 What You'll Master in This Module
Custom events (Module 6) only work between a direct parent and its direct child. But real apps often need two completely unrelated components — say, a filter panel and a results list sitting in different parts of a page, or a Lightning component needing to talk to an old Aura component — to communicate. This module covers two solutions: a simple custom Pub-Sub pattern, and Salesforce's official Lightning Message Service (LMS).
✓ Why custom events alone can't solve unrelated-component communication
✓ The Pub-Sub (Publish-Subscribe) pattern, built with a simple shared module
✓ Lightning Message Service fundamentals and the Lightning Message Channel
✓ Publishing messages with createMessageContext and publish()
✓ Subscribing to messages, and the critical unsubscribe cleanup step
✓ How LMS crosses LWC, Aura, AND Visualforce boundaries
✓ A clear decision framework: events vs pub-sub vs LMS
📋 In This Module
Concept 1 of 7
The "Unrelated Components" Problem
Recall Module 6's "prop drilling" anti-pattern — when two components have no parent-child relationship (they're siblings, or live in completely different parts of a Lightning page), custom events can't reach between them directly. The only way to force it would be relaying the event through every uninvolved component in between, which is exactly the anti-pattern we flagged.
⚡ Why This Matters
A very common real scenario: a "Filter" component and a "Results List" component placed independently on a Lightning App Builder page, with no coded relationship between them at all — App Builder just drops them both onto the same page. They still need to communicate, but events alone offer no path.
Filter Component
❓ no direct path ❓
Results List Component
Both placed independently on a Lightning page — no parent-child relationship exists between them
🛠️ Practical Mini-Implementation
This module solves this with two approaches: a lightweight custom Pub-Sub pattern (good for components you control, within the same Lightning Experience page) and Lightning Message Service (Salesforce's official, more powerful solution that also crosses into Aura and Visualforce). We'll build both, starting with Pub-Sub since it teaches the underlying concept simply before LMS adds its specific API.
⚠️ Common Gotcha
A frequent beginner mistake is trying to solve this by adding a shared parent wrapper component JUST to relay events between two otherwise-unrelated components — this technically works but adds an artificial component purely for plumbing, with no real UI purpose. If you find yourself building a "wrapper that does nothing but relay," that's the signal to reach for Pub-Sub or LMS instead.
Concept 2 of 7
The Pub-Sub (Publish-Subscribe) Pattern
Pub-Sub is a simple, classic JavaScript design pattern: a shared module maintains a list of "subscribers" interested in specific event names. Any component can "publish" an event by name, and the shared module notifies every subscriber currently listening for that name — none of the components need to know about each other directly, only about the shared module.
⚡ Why This Matters
This pattern predates LWC entirely (it was commonly used in Aura apps via a "pubsub" utility component) and understanding it from first principles makes Lightning Message Service feel like a natural, official version of the same idea — not a mysterious new API.
A SIMPLE PUB-SUB UTILITY MODULE (pubsub.js)
// pubsub.js - a shared module imported by any component that needs it
const subscribers = {}; // { eventName: [callback1, callback2, ...] }
export function subscribe(eventName, callback) {
if (!subscribers[eventName]) {
subscribers[eventName] = [];
}
subscribers[eventName].push(callback);
}
export function unsubscribe(eventName, callback) {
if (!subscribers[eventName]) return;
subscribers[eventName] = subscribers[eventName].filter(cb => cb !== callback);
}
export function publish(eventName, payload) {
if (!subscribers[eventName]) return;
subscribers[eventName].forEach(callback => callback(payload));
}
USING IT — Filter component publishes, Results component subscribes
// filterPanel.js
import { publish } from 'c/pubsub';
handleFilterChange(event) {
publish('filterchanged', { status: event.target.value });
}
// resultsList.js
import { subscribe, unsubscribe } from 'c/pubsub';
connectedCallback() {
subscribe('filterchanged', this.handleFilterChanged);
}
disconnectedCallback() {
unsubscribe('filterchanged', this.handleFilterChanged);
}
handleFilterChanged = (payload) => {
this.refreshResults(payload.status);
};
🛠️ Practical Mini-Implementation
Notice
handleFilterChanged uses the arrow function class-field syntax from Module 1, Concept 2 — this is REQUIRED here, not just a style preference, because the exact same function reference must be passed to both subscribe and unsubscribe for the cleanup to correctly remove the right callback. A regular method would still technically work for this specific case since it's called via this.handleFilterChanged consistently, but the arrow function habit prevents subtle "this" bugs if the callback is ever passed around differently.⚠️ Common Gotcha
This custom Pub-Sub module ONLY works between Lightning Web Components that import the same shared module — it has no built-in mechanism to reach Aura components or Visualforce pages. This is the key limitation that Lightning Message Service (Concept 3 onward) was specifically built to solve.
Concept 3 of 7
Lightning Message Service Fundamentals
Lightning Message Service (LMS) is Salesforce's official, supported version of the Pub-Sub idea — built around a metadata definition called a Lightning Message Channel, which acts as the "named topic" that publishers and subscribers both reference, similar to the event name string in our custom Pub-Sub module, but now formally defined as a deployable metadata component.
⚡ Why This Matters
Because the Message Channel is metadata (not just a string in JavaScript), it's discoverable, versioned, and deployable like any other Salesforce component — and critically, it's recognized by Aura and Visualforce too, not just LWC.
DEFINING A LIGHTNING MESSAGE CHANNEL (metadata file)
<!-- FilterChange.messageChannel-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
<masterLabel>FilterChange</masterLabel>
<isExposed>true</isExposed>
<lightningMessageFields>
<fieldName>status</fieldName>
</lightningMessageFields>
</LightningMessageChannel>
// This file lives in the messageChannels/ folder, named FilterChange.messageChannel-meta.xml
IMPORTING THE MESSAGE CHANNEL IN A COMPONENT
import { LightningElement, wire } from 'lwc';
import {
subscribe, unsubscribe, publish, createMessageContext, releaseMessageContext
} from 'lightning/messageService';
import FILTER_CHANGE_CHANNEL from '@salesforce/messageChannel/FilterChange__c';
// Note the __c suffix on the import — even though you didn't type it in the XML file name
🛠️ Practical Mini-Implementation
All the LMS functions (
subscribe, unsubscribe, publish, createMessageContext) come from the SAME import source: 'lightning/messageService' — a single, consistent place to import everything you need, unlike our custom Pub-Sub module where we wrote each function ourselves.⚠️ Common Gotcha
The Message Channel import path always ends in
__c (e.g., FilterChange__c), mirroring how custom objects and fields work in Salesforce, even though you never typed __c anywhere in the channel's metadata file itself — Salesforce appends it automatically as the channel's API name suffix.Concept 4 of 7
Publishing Messages with LMS
To publish a message, you first need a MessageContext — created via
createMessageContext() — which represents your component's connection to the messaging system. You then call publish(), passing the context, the imported Message Channel, and a payload object matching the fields defined in the channel's metadata.⚡ Why This Matters
The MessageContext step has no equivalent in our simple custom Pub-Sub module — it exists because LMS needs to track which component (and which part of the DOM, including across Aura/Visualforce boundaries) is publishing, for proper message routing and cleanup.
PUBLISHING A MESSAGE — Filter component
import { LightningElement } from 'lwc';
import { publish, createMessageContext } from 'lightning/messageService';
import FILTER_CHANGE_CHANNEL from '@salesforce/messageChannel/FilterChange__c';
export default class FilterPanel extends LightningElement {
messageContext = createMessageContext(); // created once, when the component initializes
handleFilterChange(event) {
const payload = { status: event.target.value };
publish(this.messageContext, FILTER_CHANGE_CHANNEL, payload);
}
}
// payload field names ("status") must match the lightningMessageFields defined in the channel's XML
🛠️ Practical Mini-Implementation
Calling
createMessageContext() as a class field initializer (as shown above) means it runs once when the component instance is created, giving you a single reusable context for the component's entire lifetime — you don't need to call it again before every single publish() call.⚠️ Common Gotcha
The payload object's keys must exactly match the field names defined in the Message Channel's metadata (
lightningMessageFields) — sending a field name that wasn't declared in the channel definition will be silently ignored by subscribers rather than throwing an error, making mismatched field names a frustrating, silent bug to track down.Concept 5 of 7
Subscribing & the Unsubscribe Gotcha
Subscribing follows a similar pattern to publishing — you need a MessageContext and call
subscribe() with the channel and a callback function. Just like the custom Pub-Sub module in Concept 2, and just like Module 2's lifecycle cleanup lesson, you MUST unsubscribe when the component is removed, or you risk a callback firing on a component that no longer exists.⚡ Why This Matters
LMS subscriptions are a textbook example of the Module 2 disconnectedCallback cleanup pattern — if you forget to unsubscribe, the subscription keeps the component (and its memory) alive longer than it should, and the callback may try to update a component instance that's no longer rendered.
SUBSCRIBING — Results List component, with proper cleanup
import { LightningElement } from 'lwc';
import {
subscribe, unsubscribe, createMessageContext, releaseMessageContext
} from 'lightning/messageService';
import FILTER_CHANGE_CHANNEL from '@salesforce/messageChannel/FilterChange__c';
export default class ResultsList extends LightningElement {
messageContext = createMessageContext();
subscription;
connectedCallback() {
this.subscription = subscribe(
this.messageContext,
FILTER_CHANGE_CHANNEL,
(payload) => this.handleFilterMessage(payload)
);
}
disconnectedCallback() {
unsubscribe(this.subscription); // stop receiving this specific subscription
releaseMessageContext(this.messageContext); // release the context entirely
}
handleFilterMessage(payload) {
this.refreshResults(payload.status);
}
}
🛠️ Practical Mini-Implementation
Notice
subscribe() RETURNS a subscription object, which you store and later pass to unsubscribe() — this is different from our custom Pub-Sub module where we passed the same callback reference to both functions. LMS's approach (returning a subscription handle) is slightly more robust since it doesn't depend on function reference equality.⚠️ Common Gotcha
Forgetting
releaseMessageContext() alongside unsubscribe() is a subtle memory-management gap — unsubscribing stops that ONE subscription from receiving messages, but releasing the context cleans up the broader connection your component established with the messaging system. Both cleanup calls belong in disconnectedCallback() together.Concept 6 of 7
Cross-DOM: Reaching Aura and Visualforce
This is LMS's headline feature that our custom Pub-Sub module (Concept 2) cannot do: a Lightning Web Component can publish a message that an Aura component OR a Visualforce page subscribes to, and vice versa — letting old and new Salesforce UI technologies coexist and communicate during a gradual migration.
⚡ Why This Matters
Many real Salesforce orgs have years of existing Aura components and Visualforce pages that can't realistically be rewritten overnight. LMS lets teams introduce new LWC components that genuinely integrate with that legacy UI, rather than living in complete isolation from it.
CONCEPTUAL EXAMPLE — Aura component subscribing to the same channel
// Aura uses a similar but Aura-flavored syntax (lightning:messageChannel, etc.)
// The KEY point: it references the SAME Message Channel metadata component
// (FilterChange__c) that our LWC components use — that shared metadata
// definition is what makes cross-framework communication possible.
// Conceptually:
// LWC FilterPanel --publish--> FilterChange__c channel --notify--> Aura ResultsList
// Aura ResultsList --publish--> FilterChange__c channel --notify--> LWC FilterPanel
// Communication works in EITHER direction, regardless of which framework initiates it
🛠️ Practical Mini-Implementation
You don't need to memorize the exact Aura-side syntax for this course — the important takeaway for an LWC-focused course (and for interviews) is recognizing WHY this capability exists and WHEN it matters: specifically during Aura-to-LWC migrations, where new and old components must temporarily coexist and stay in sync.
⚠️ Common Gotcha
Don't assume LMS automatically solves EVERY cross-framework problem — it specifically handles messaging/notification between components. It does not, for example, let an LWC component directly call an Aura component's methods or share Apex-backed data caching between frameworks the way Lightning Data Service does within LWC alone.
Concept 7 of 7
Events vs Pub-Sub vs LMS — Decision Guide
You now know three communication tools from Modules 6-7. Let's build a clear decision framework so you reach for the right one immediately, instead of guessing.
⚡ Why This Matters
This exact comparison — "when would you use X instead of Y" — is one of the most common Phase 3 interview questions, because it tests whether you understand the TRADE-OFFS, not just the syntax of each tool individually.
| Scenario | Use This | Why |
|---|---|---|
| Direct parent ↔ child | Custom Events (M6) | Simplest, most direct, no extra setup needed |
| Two unrelated LWC components, same page | Pub-Sub utility | Lightweight, no metadata to deploy, full control |
| Components possibly reused across many unrelated pages/apps | Lightning Message Service | Formal, deployable, more discoverable than an ad-hoc JS module |
| LWC needs to talk to an Aura component | Lightning Message Service | Only LMS crosses this boundary |
| LWC needs to talk to a Visualforce page | Lightning Message Service | Only LMS crosses this boundary |
🛠️ Practical Mini-Implementation
A simple mental shortcut: "Is there a direct parent-child relationship? → Events. Are both components LWC-only and I want something quick and lightweight? → Pub-Sub. Do I need official tooling, broader reuse, or to cross into Aura/Visualforce? → LMS." Walking through this checklist out loud is a strong way to answer the inevitable interview question on this topic.
⚠️ Common Gotcha — Module Wrap-Up
Don't default to LMS for everything just because it's the "official" Salesforce tool — it has more setup overhead (a metadata component to create and deploy) than custom events or a simple Pub-Sub module. Use the lightest tool that solves your actual problem; reserve LMS specifically for when you need its unique cross-framework or broader-reuse capabilities.
💬 Module 7 Interview Questions (6)
Q1Two Lightning Web Components are placed independently on the same Lightning App Builder page with no parent-child relationship. Why can't custom events solve communication between them, and what are your two options?
Custom events only travel from a child to its direct parent listening on its tag — they have no mechanism to reach a sibling component with no coded relationship to the publisher, since neither component has a reference to the other or a shared ancestor actively relaying the event. The two options are implementing a custom Pub-Sub pattern using a shared JavaScript module that both components import, or using Lightning Message Service, Salesforce's official messaging system, which is the more robust and broadly supported solution for this scenario.
"Custom events only work between direct parent and child; unrelated components need either a custom Pub-Sub module both components import, or Lightning Message Service, Salesforce's official solution for this exact problem."
Q2In a custom Pub-Sub implementation, why is it important to pass the exact same function reference to both subscribe() and unsubscribe()?
The custom Pub-Sub module typically stores subscriber callbacks in an array and removes a specific callback from that array by comparing function references (using something like Array.filter with a reference equality check). If a different function reference is passed to unsubscribe() than the one originally passed to subscribe() — for example, an inline arrow function defined freshly each time rather than a stored class field arrow function — the removal will fail silently, since the stored reference won't match, leaving the original subscription active and potentially causing memory leaks or unexpected callback firing after a component should have stopped listening.
"Custom Pub-Sub modules remove subscribers by comparing function references, so subscribe() and unsubscribe() must use the exact same stored reference (typically a class field arrow function) — a freshly created inline function won't match and the unsubscribe will silently fail."
Q3What is a Lightning Message Channel, and what makes it different from simply using a string event name in a custom Pub-Sub module?
A Lightning Message Channel is a formal, deployable metadata component that defines a named messaging "topic" along with the specific fields a published message can carry, unlike a plain string event name in custom Pub-Sub which exists only as JavaScript code with no formal definition, no field validation, and no discoverability outside the code itself. Because the Message Channel is metadata, it can be deployed, versioned, and referenced consistently across LWC, Aura, and Visualforce, whereas a custom Pub-Sub string-based event name only has meaning within the JavaScript modules that explicitly import and use it.
"A Lightning Message Channel is a deployable metadata definition of a messaging topic and its fields, making it discoverable and usable across LWC, Aura, and Visualforce — unlike a plain string event name in custom Pub-Sub, which only has meaning within the specific JavaScript modules using it."
Q4What are the two separate cleanup calls needed when unsubscribing from a Lightning Message Service subscription in disconnectedCallback(), and what does each one do?
The two calls are unsubscribe(), which stops that specific subscription from continuing to receive published messages on the channel, and releaseMessageContext(), which releases the broader MessageContext connection the component established with the messaging system entirely. Both should be called together in disconnectedCallback() — unsubscribe() alone stops the targeted subscription, but releaseMessageContext() additionally cleans up the underlying context object, and omitting it is a subtle memory-management gap that can leave residual connections active even after the specific subscription has been removed.
"unsubscribe() stops the specific subscription from receiving further messages, while releaseMessageContext() separately cleans up the broader messaging connection the component established — both should be called together in disconnectedCallback() for complete cleanup."
Q5Why is Lightning Message Service specifically valuable during an Aura-to-LWC migration project, in a way that custom Pub-Sub or custom events cannot replicate?
During a migration, an organization typically has both old Aura components and new LWC components coexisting on the same pages for an extended transition period, and these components frequently need to communicate with each other to maintain a consistent user experience. Custom events cannot cross between Aura and LWC at all, and a custom Pub-Sub module written in LWC JavaScript has no native mechanism for an Aura component to import and use it. Lightning Message Service is specifically designed to cross this boundary, since both LWC and Aura have native support for subscribing to and publishing on the same underlying Lightning Message Channel metadata component, allowing old and new components to communicate seamlessly during the gradual migration period.
"Lightning Message Service is uniquely valuable during Aura-to-LWC migrations because it is natively supported by both frameworks referencing the same Message Channel metadata, allowing old Aura components and new LWC components to communicate seamlessly — something neither custom events nor a custom Pub-Sub module can achieve across that framework boundary."
Q6A developer defaults to using Lightning Message Service for every cross-component communication scenario in a new LWC-only application, including straightforward parent-child cases. Is this a good practice, and why or why not?
This is generally not considered good practice, since Lightning Message Service introduces more overhead — specifically requiring a deployable metadata component (the Message Channel) and a more involved API (MessageContext creation, subscription handles) — compared to simpler, lighter-weight tools that are perfectly sufficient for their specific scenarios. For a direct parent-child relationship, a custom event is simpler and requires no additional metadata at all. For unrelated LWC-only components within the same application where official tooling and cross-framework support aren't needed, a lightweight custom Pub-Sub module is often sufficient. Lightning Message Service should be reserved for scenarios where its specific strengths are actually needed — broader reuse, more formal governance, or genuinely crossing into Aura or Visualforce — rather than being used as a default for every communication need regardless of complexity.
"Defaulting to Lightning Message Service for every scenario is not ideal, since it adds unnecessary metadata and API overhead for simple cases — custom events remain the right tool for direct parent-child relationships, and a lightweight custom Pub-Sub module is often sufficient for unrelated LWC-only components, reserving LMS for when its specific cross-framework or broader-reuse strengths are genuinely needed."
📝 Module 7 Recap — Cross-Component Communication Mastered
✅ Custom events only solve direct parent-child communication — unrelated components need a different tool
✅ A custom Pub-Sub module (subscribe/unsubscribe/publish) is a lightweight solution for unrelated LWC-only components
✅ Lightning Message Service formalizes this with a deployable Lightning Message Channel metadata component
✅ Publishing requires a MessageContext (via createMessageContext) plus publish() with a matching payload shape
✅ Subscribing requires the same context, and MUST be paired with unsubscribe() AND releaseMessageContext() in disconnectedCallback()
✅ LMS uniquely crosses LWC, Aura, and Visualforce boundaries — critical during migration projects
✅ Decision framework: direct parent-child → events; unrelated LWC-only → Pub-Sub; cross-framework or formal reuse → LMS
🎯 Before Moving to Module 8...
Try this: build the FilterPanel and ResultsList example from this module using BOTH approaches — first with the custom Pub-Sub module from Concept 2, then refactor it to use Lightning Message Service from Concepts 3-5. Comparing the two side by side, in your own code, makes the trade-offs from Concept 7's decision guide concrete rather than theoretical. Module 8 covers Navigation, Modals, and Toasts — the remaining communication-adjacent tools every LWC developer needs for a complete, polished user experience.
☕
☕ Enjoyed this article?
SF Interview Pro is 100% free and maintained by a Salesforce professional. No ads, no paywalls, and no signup required. If this guide helped you prepare for an interview, earn a certification, or grow your Salesforce career, consider buying me a coffee! ☕💜
🇮🇳 UPI (India)
Pay by QR
GPay · PhonePe · Paytm · BHIM
📚 Keep Preparing
New interview questions every week 🚀
Follow for fresh Salesforce Q&A, free courses, and real interview experiences — straight from the trenches.
👥 Follow on LinkedIn