lightning/empApi module allows your component to listen to these streaming events in real time without refreshing the page or repeatedly polling the server.
Modern enterprise applications rely on event-driven architecture to keep user interfaces updated in real time. By subscribing to Platform Events directly inside an LWC via streaming APIs, you can trigger UI toast alerts, refresh datatables, or update progress bars the moment external systems or asynchronous Apex jobs complete.
Prerequisites
- A Salesforce Developer Edition, Sandbox, or Scratch Org.
- Salesforce CLI (
sf) installed and authenticated to your org. - Basic understanding of LWC component lifecycle hooks (
connectedCallbackanddisconnectedCallback).
Step 1: Define the Platform Event Schema
First, create the Platform Event definition in Salesforce Setup:
- Navigate to Setup > Platform Events and click New Platform Event.
- Set Label to
Order Notificationand Plural Label toOrder Notifications(API Name:Order_Notification__e). - Set Publish Behavior to Publish After Commit.
- Create a custom text field:
Message__c(Length: 255). - Save the event definition.
Step 2: Build the LWC Event Subscriber Component
Generate a new Lightning Web Component named platformEventListener:
sf lightning generate component -n platformEventListener -d force-app/main/default/lwc --type lwc
In platformEventListener.html, create the UI to display subscription status and received real-time event payloads:
<template>
<lightning-card title="Real-Time Platform Event Monitor" icon-name="utility:broadcast">
<div class="slds-p-around_medium">
<div class="slds-m-bottom_small">
<p class="slds-text-body_regular">
Listening Channel: <code class="ap-code">{channelName}</code>
</p>
<p class="slds-text-body_regular slds-m-top_xx-small">
Subscription Status:
<strong class={subscriptionStatusClass}>{subscriptionStatusText}</strong>
</p>
</div>
<div class="slds-button-group slds-m-bottom_medium" role="group">
<lightning-button
variant="brand"
label="Subscribe"
onclick={handleSubscribe}
disabled={isSubscribed}>
</lightning-button>
<lightning-button
variant="neutral"
label="Unsubscribe"
onclick={handleUnsubscribe}
disabled={isNotSubscribed}>
</lightning-button>
<lightning-button
variant="destructive-text"
label="Clear Log"
onclick={handleClearLogs}
disabled={isLogEmpty}>
</lightning-button>
</div>
<!-- Event Log Output -->
<div class="slds-box slds-theme_shade">
<h3 class="slds-text-heading_small slds-m-bottom_x-small">Received Event Stream:</h3>
<template lwc:if={hasEvents}>
<ul class="slds-list_dotted">
<template for:each={receivedEvents} for:item="evt">
<li key={evt.id} class="slds-m-bottom_xx-small">
<span class="slds-text-color_weak">[{evt.time}]</span>
<strong>{evt.message}</strong> (Replay ID: {evt.replayId})
</li>
</template>
</ul>
</template>
<template lwc:else>
<p class="slds-text-color_weak">No events received yet. Waiting for incoming stream...</p>
</template>
</div>
</div>
</lightning-card>
</template>
Step 3: Implement empApi Streaming Logic in JavaScript
In platformEventListener.js, import subscribe, unsubscribe, and onError from lightning/empApi. Ensure the subscription object is retained for clean teardown during disconnectedCallback:
import { LightningElement, track } from 'lwc';
import { subscribe, unsubscribe, onError } from 'lightning/empApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class PlatformEventListener extends LightningElement {
channelName = '/event/Order_Notification__e';
isSubscribed = false;
subscription = {};
@track receivedEvents = [];
get isNotSubscribed() {
return !this.isSubscribed;
}
get hasEvents() {
return this.receivedEvents.length > 0;
}
get isLogEmpty() {
return this.receivedEvents.length === 0;
}
get subscriptionStatusText() {
return this.isSubscribed ? 'Active (Listening)' : 'Inactive';
}
get subscriptionStatusClass() {
return this.isSubscribed ? 'slds-text-color_success' : 'slds-text-color_error';
}
connectedCallback() {
this.registerErrorListener();
this.handleSubscribe();
}
disconnectedCallback() {
this.handleUnsubscribe();
}
handleSubscribe() {
if (this.isSubscribed) return;
// Callback invoked whenever an event is published
const messageCallback = (response) => {
const payload = response.data.payload;
const newEvent = {
id: response.data.schema + '-' + response.data.event.replayId,
message: payload.Message__c || 'No message provided',
replayId: response.data.event.replayId,
time: new Date().toLocaleTimeString()
};
this.receivedEvents = [newEvent, ...this.receivedEvents];
this.showToast('Event Received', payload.Message__c, 'info');
};
// Replay from -1 (new events only)
subscribe(this.channelName, -1, messageCallback).then((response) => {
this.subscription = response;
this.isSubscribed = true;
}).catch(error => {
this.showToast('Subscription Error', JSON.stringify(error), 'error');
});
}
handleUnsubscribe() {
if (!this.isSubscribed || !this.subscription.subscription) return;
unsubscribe(this.subscription, (response) => {
this.isSubscribed = false;
this.subscription = {};
}).catch(error => {
this.showToast('Unsubscribe Error', JSON.stringify(error), 'error');
});
}
handleClearLogs() {
this.receivedEvents = [];
}
registerErrorListener() {
onError((error) => {
console.error('Streaming API Error: ', JSON.stringify(error));
});
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
}
unsubscribe() will fail. You must pass the subscription object returned by the subscribe() promise resolution. Always clear subscriptions inside disconnectedCallback() to prevent memory leaks and orphaned listeners.
- Replay Options: Pass
-1to receive only new events generated after subscribing, or-2to replay all retained events within the 24-hour retention window. - Streaming Limits: Event delivery counts against your daily CometD/Bayeux streaming client allocation limits.
- Publishing Engine: Use Apex
EventBus.publish(), Salesforce Flows, or external REST/PubSub APIs to publish event payloads.
Step 4: Expose, Deploy, and Test the Event Stream
Update platformEventListener.js-meta.xml to expose the component to Lightning App Builder:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
- Deploy changes:
sf project deploy start. - Open Lightning App Builder and drag
platformEventListeneronto your target page layout. - Open the Developer Console (or execute via VS Code CLI) and publish a test event using Anonymous Apex:
// Test Platform Event Publish in Anonymous Apex
Order_Notification__e evt = new Order_Notification__e(
Message__c = 'Order #89421 successfully processed by billing gateway.'
);
Database.SaveResult sr = EventBus.publish(evt);
System.debug('Event published successfully: ' + sr.isSuccess());
- Observe the incoming notification card update in real time on the UI without reloading the browser tab.
lightning/empApi provides clean, real-time client-side event notifications, eliminating the overhead of manual polling and server round-trips.