Skip to main content

Managing Wire Adapter Caching and Refresh in LWC

The @wire decorator in Lightning Web Components is backed by the Lightning Data Service (LDS) cache. Understanding this caching behavior is essential for building components that display accurate data after mutations.

How Wire Caching Works

When a wire adapter fetches data, the result is stored in the LDS client-side cache. Subsequent renders of the same component (or other components using the same adapter with identical parameters) will receive the cached result immediately rather than making a new server call.

This cache is shared across all components on the page. If Component A and Component B both wire the same Apex method with the same parameters, they share one cached result.

The Stale Data Problem

Wire adapters marked with @AuraEnabled(cacheable=true) do not automatically refresh after imperative DML operations. If your component calls an Apex method to update records and then expects the wired data to reflect those changes, the display will still show the cached (stale) values.

// This will NOT refresh the wired data
import getLineItems from "@salesforce/apex/LineItemController.getLineItems";
import updateQuantity from "@salesforce/apex/LineItemController.updateQuantity";

export default class LineItemTable extends LightningElement {
@api recordId;
lineItems;

@wire(getLineItems, { orderId: "$recordId" })
wiredLineItems({ data, error }) {
if (data) {
this.lineItems = data;
}
}

async handleSave(newQuantity) {
await updateQuantity({ itemId: this.selectedId, qty: newQuantity });
// lineItems still shows old data -- cache has not been invalidated
}
}

The refreshApex Pattern

The solution is to store the entire wired result (not just the data) and pass it to refreshApex().

import { LightningElement, api, wire } from "lwc";
import { refreshApex } from "@salesforce/apex";
import getLineItems from "@salesforce/apex/LineItemController.getLineItems";
import updateQuantity from "@salesforce/apex/LineItemController.updateQuantity";

export default class LineItemTable extends LightningElement {
@api recordId;
lineItems;
_wiredResult; // Store the full wire result

@wire(getLineItems, { orderId: "$recordId" })
wiredLineItems(result) {
this._wiredResult = result; // Store { data, error } object
if (result.data) {
this.lineItems = result.data;
}
}

async handleSave(newQuantity) {
await updateQuantity({ itemId: this.selectedId, qty: newQuantity });
await refreshApex(this._wiredResult);
// lineItems is now updated with fresh server data
}
}

The critical detail is that refreshApex() must receive the same object reference that the wire provisioned. Passing just this.lineItems (the extracted data) will not work.

Delayed Refresh for Async Backend Processing

Some operations trigger asynchronous server-side processing (repricing calculations, flow automations, rollup recalculations). In these cases, an immediate refreshApex may return data that has not yet been updated.

async handleReprice() {
this.isProcessing = true;
try {
await triggerRepricing({ quoteId: this.recordId });

// Wait for async repricing to complete
// eslint-disable-next-line @lwc/lwc/no-async-operation
await new Promise((resolve) => setTimeout(resolve, 2000));

await refreshApex(this._wiredResult);
} finally {
this.isProcessing = false;
}
}

The @lwc/lwc/no-async-operation ESLint rule flags setTimeout usage because it is generally an anti-pattern in LWC. However, waiting for known asynchronous backend processing is a legitimate exception. Suppress it with an inline comment explaining the reason.

A more robust alternative is to poll until the data reflects the expected state:

async waitForRepricingComplete(maxAttempts = 5) {
for (let i = 0; i < maxAttempts; i++) {
await refreshApex(this._wiredResult);
if (this._wiredResult.data?.some((item) => item.Is_Repriced__c)) {
return true;
}
// eslint-disable-next-line @lwc/lwc/no-async-operation
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return false;
}

getRecordNotifyChange for Single Records

When you update a single record and want to refresh all wire adapters that depend on it, getRecordNotifyChange is a lighter-weight alternative:

import { getRecordNotifyChange } from "lightning/uiRecordApi";

async handleFieldUpdate() {
await updateRecord({ fields: { Id: this.recordId, Status__c: "Active" } });
getRecordNotifyChange([{ recordId: this.recordId }]);
}

This invalidates the LDS cache for that specific record ID. All components on the page using getRecord or getFieldValue with that record will re-fetch automatically. However, it does not refresh custom Apex wire adapters -- use refreshApex for those.

Summary

ScenarioMethod
Refresh custom Apex wire after DMLrefreshApex(this._wiredResult)
Refresh standard LDS wires after DMLgetRecordNotifyChange([{ recordId }])
Refresh after async processingsetTimeout + refreshApex (with lint suppression)
Force re-query with new parametersChange a reactive $ property

Always store the full wire result reference when you anticipate needing to refresh the data. Extracting only the data property in the wire handler is the most common source of refreshApex failures.