Workaround: Refresh Custom LWC When Data Changes in Standard Components or Backend (Platform Events + refreshApex)

A simple, reliable workaround to refresh a custom Lightning Web Component (LWC) when data changes in standard Salesforce components or via backend updates using Platform Events and refreshApex.

Problem Overview

By default, refreshApex() refreshes data for wired Apex in an LWC, and refreshView APIs can refresh standard components. However, there is no out‑of‑the‑box mechanism for notifying a custom LWC when data changes in a standard component or is updated from backend Apex/automation. This post shows a lightweight workaround using Platform Events to publish changes from server-side code and subscribe from your LWC to call refreshApex().

Solution Summary

The idea is to:

  • Create a Platform Event (for example: Refresh_Custom_Components__e).
  • Publish that event from a trigger or Flow when relevant records change.
  • Subscribe to the Platform Event in your LWC using lightning/empApi and call refreshApex() on the wired property when the event is received.

Step-by-step Implementation

1) Create the Platform Event

Create a Platform Event named Refresh_Custom_Components__e. No special fields are required for this pattern; a simple event acts as a broadcast signal.

2) Publish the Platform Event from Apex Trigger

Use a trigger (or Flow) to publish the platform event when record changes occur. Only publish the event for the fields or operations you care about to avoid noisy notifications.

trigger OpportunityTrigger on Opportunity (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
    OpportunityTriggerHandler handler = new OpportunityTriggerHandler(Trigger.isExecuting, Trigger.size);
    switch on Trigger.operationType {
        when BEFORE_INSERT {
            // handler.beforeInsert(Trigger.new);
        } 
        when BEFORE_UPDATE {
            // handler.beforeUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap);
        }
        when BEFORE_DELETE {
            // handler.beforeDelete(Trigger.old, Trigger.oldMap);
        }
        when AFTER_INSERT {
            // handler.afterInsert(Trigger.new, Trigger.newMap);
        }
        when AFTER_UPDATE {
            handler.afterUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap);
        }
        when AFTER_DELETE {
            // handler.afterDelete(Trigger.old, Trigger.oldMap);
        } 
        when AFTER_UNDELETE {
            // handler.afterUndelete(Trigger.new, Trigger.newMap);
        }
    }
}
public without sharing class OpportunityTriggerHandler {
    private boolean triggerIsExecuting=false;
    private integer triggerSize=0;
    public OpportunityTriggerHelper helper;
    public OpportunityTriggerHandler(boolean triggerIsExecuting, integer triggerSize) {
        this.triggerIsExecuting = triggerIsExecuting;
        this.triggerSize = triggerSize;
        this.helper = new OpportunityTriggerHelper();
    }
    public void beforeInsert(List<Opportunity> newOpportunities) {
        // helper.doTask1();
    }
    public void beforeUpdate(List<Opportunity> oldOpportunities, List<Opportunity> newOpportunities, Map<ID, SObject> oldOpportunitiesMap, Map<ID, SObject> newOpportunitiesMap) {
        // helper.doTask3();
    }
    public void afterUpdate(List<Opportunity> oldOpportunities, List<Opportunity> newOpportunities, Map<ID, SObject> oldOpportunityMap, Map<ID, SObject> newOpportunityMap) {
        helper.componentRefreshPE();
    }
}
public with sharing class OpportunityTriggerHelper {
    public OpportunityTriggerHelper() {
        System.debug('Inside OpportunityTriggerHelper Constructor');
    }
    public void componentRefreshPE() {
        System.debug('componentRefreshPE');
        Refresh_Custom_Components__e refreshEvent = new Refresh_Custom_Components__e();
        EventBus.publish(refreshEvent);
    }
}

3) Expose a Cacheable Apex Method For Your LWC

Create an @AuraEnabled(cacheable=true) Apex method that returns the data your component shows. This method will be wired in the LWC and refreshed via refreshApex().

public with sharing class OpptiesOverAmountApex {
    @AuraEnabled(cacheable=true)
    public static List<Opportunity> getOpptyOverAmount(Id recordId) {
        return [SELECT Id, Name, Amount, StageName, CloseDate FROM Opportunity WHERE AccountId=:recordId];
    }
}

4) LWC: Subscribe to Platform Event and Call refreshApex()

Use lightning/empApi to subscribe to the platform event channel (for example: /event/Refresh_Custom_Components__e). When an event arrives, call your component’s refresh method which invokes refreshApex on the wired property.

// opportunitiesOverAmount.js
import { LightningElement, api, wire } from "lwc";
import { refreshApex } from "@salesforce/apex";
import getOppty from "@salesforce/apex/OpptiesOverAmountApex.getOpptyOverAmount";
import {
    subscribe,
    unsubscribe,
    onError,
    setDebugFlag,
    isEmpEnabled,
} from 'lightning/empApi';

export default class OpportunitiesOverAmount extends LightningElement {
  @api recordId;
  @api channelName = '/event/Refresh_Custom_Components__e';
  @wire(getOppty, { recordId: "$recordId" })
  getOppty;

    connectedCallback() {  
        const self = this;
        const callbackFunction = function (response) {
            console.log("🚀 ~ callbackFunction:"+JSON.stringify(response));
            self.refreshMyData();
        }
        subscribe(this.channelName, -1, callbackFunction).then(response => {
            console.log("🚀 ~ subscribe:"+JSON.stringify(response));
        });

    }
    refreshMyData() { 
        refreshApex(this.getOppty).then(() => {
            console.log('###Refreshing Data');    
        });
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

Best Practices and Tips

  • Publish events only for specific changes (e.g., when particular fields change) to avoid excessive notifications.
  • Consider debouncing or throttling updates if your trigger fires frequently.
  • Use platform events for lightweight signaling; don’t rely on them to carry large payloads or replace transactional communication.
  • If business logic is simple, consider Flows to publish Platform Events to avoid extra Apex.

When to Use This Pattern

Use this pattern when you need to keep a custom LWC in sync with changes made by standard components (like related lists) or backend processes (Apex/Flows) and you can’t change the standard component’s behavior. It’s especially useful on record pages where multiple components (standard and custom) coexist.

Conclusion

Using Platform Events as a broadcast mechanism, and refreshApex() on the LWC side, gives a reliable and scalable way to refresh custom LWCs when standard components or backend processes update data. This keeps the UI consistent without resorting to heavy polling or coupling components tightly.

Why this matters: keeping data fresh across standard and custom UI improves user confidence, reduces confusion, and enables richer real-time experiences for Salesforce admins, developers, and end users.