LWC Zero to Hero Module 11 — Caching, Refresh & Advanced Wire Patterns
🔁 LWC Zero to Hero — Module 11 of 15
Caching, Refresh & Advanced Wire Patterns
Module 10 left us with a gap: wired data going stale after an imperative change. This module closes that gap — and completes Phase 4.
Module 11 of 15 (counting Module 0 as a bonus prerequisite) · Phase 4: Data Integration (Final Module)
🎯 What You'll Master in This Module
We end Phase 4 by tying @wire and imperative calls together properly. You'll learn the specific tool for manually refreshing stale wired data, understand how Salesforce's built-in Lightning Data Service cache actually behaves (and when it saves you from needing to think about staleness at all), and build a complete mental model for choosing the right data strategy in any real component.
✓ The stale data problem, precisely — recap and root cause
✓ refreshApex() — the tool that manually re-triggers a wired property
✓ The complete pattern: imperative action followed by refreshApex()
✓ Lightning Data Service's shared, automatic caching behavior
✓ Why LDS-managed data auto-updates across components, while custom Apex wires don't
✓ cacheable=true cache duration, scope, and invalidation behavior
✓ A complete, confident data integration decision framework
📋 In This Module
Concept 1 of 7
The Stale Data Problem, Precisely
Recall Module 10's gotcha: a wired property only re-fetches when its REACTIVE PARAMETERS change (Module 9, Concept 4) — it has no awareness that some unrelated imperative Apex call just modified the exact data it's displaying. The wire service isn't broken; it's working exactly as designed, just not for this specific cross-cutting scenario.
⚡ Why This Matters
This is one of THE most common real-world LWC bugs reported by teams — "I saved the record but the page still shows old data" — and it has a precise, well-understood cause and fix, which is exactly what this module provides.
REPRODUCING THE EXACT PROBLEM
export default class OrderDashboard extends LightningElement {
@api recordId;
@wire(getOrdersForAccount, { accountId: '$recordId' })
orders; // displays the order list
async handleStatusUpdate(orderId) {
await updateOrderStatus({ orderId, status: 'Shipped' }); // Module 10's imperative call
// "orders" above is now STALE — recordId never changed, so the wire never re-ran
// The screen still shows the OLD status, even though the database has the new one
}
}
🛠️ Practical Mini-Implementation
Notice the root cause precisely: the wire's reactive parameter (
recordId) genuinely didn't change — only the underlying DATA changed, through a completely separate code path. The wire service has no way to know this happened unless you explicitly tell it to.⚠️ Common Gotcha
A common but WRONG "fix" some developers try is forcing a wire re-run by reassigning the reactive parameter to itself (like
this.recordId = this.recordId) — this is unreliable, hacky, and doesn't actually guarantee a re-fetch in all cases. The correct tool is refreshApex(), covered next.Concept 2 of 7
refreshApex() Fundamentals
refreshApex() is a utility function that takes the WIRE RESULT itself (not just any property) and forces it to re-run, bypassing the normal "only re-runs when reactive parameters change" rule. It's imported from @salesforce/apex, the same module family as your Apex method imports.⚡ Why This Matters
This is the official, reliable, platform-supported way to solve Concept 1's exact problem — no hacks, no workarounds, just a purpose-built tool.
THE CRITICAL REQUIREMENT — you need the FULL wire result, not a destructured copy
import { refreshApex } from '@salesforce/apex';
export default class OrderDashboard extends LightningElement {
@api recordId;
wiredOrdersResult; // will store the FULL result object, not just the data
@wire(getOrdersForAccount, { accountId: '$recordId' })
wiredOrders(result) {
this.wiredOrdersResult = result; // store the WHOLE thing — needed for refreshApex later
if (result.data) {
this.orders = result.data;
}
}
async handleStatusUpdate(orderId) {
await updateOrderStatus({ orderId, status: 'Shipped' });
await refreshApex(this.wiredOrdersResult); // ✅ forces the wire to re-fetch fresh data
}
}
🛠️ Practical Mini-Implementation
This is exactly why function form (Module 9, Concept 2) is sometimes necessary even when property form would otherwise suffice — you need access to the FULL
result object (not just result.data) so you have something valid to later pass into refreshApex().⚠️ Common Gotcha
Passing only
this.orders (the extracted data) to refreshApex() instead of the full stored wire result object will fail — refreshApex() specifically needs the original wire result reference, with its internal tracking metadata intact, not just the plain data you extracted from it.Concept 3 of 7
The Complete Imperative + Refresh Pattern
Let's combine everything from Modules 9-11 into one complete, production-quality pattern: wired display data, an imperative update action, proper error handling, and a refresh to keep everything in sync afterward.
⚡ Why This Matters
This is genuinely how most real, data-driven LWC components are built — this single pattern is worth internalizing completely, since you'll reuse it constantly.
THE FULL PATTERN, PUTTING IT ALL TOGETHER
import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getOrdersForAccount from '@salesforce/apex/OrderController.getOrdersForAccount';
import updateOrderStatus from '@salesforce/apex/OrderController.updateOrderStatus';
export default class OrderDashboard extends LightningElement {
@api recordId;
orders;
wiredOrdersResult;
isUpdating = false;
@wire(getOrdersForAccount, { accountId: '$recordId' })
wiredOrders(result) {
this.wiredOrdersResult = result;
if (result.data) this.orders = result.data;
}
async handleStatusUpdate(orderId) {
this.isUpdating = true;
try {
await updateOrderStatus({ orderId, status: 'Shipped' });
await refreshApex(this.wiredOrdersResult); // keeps the displayed list in sync
this.dispatchEvent(new ShowToastEvent({
title: 'Updated', message: 'Order marked as shipped.', variant: 'success'
}));
} catch (error) {
this.dispatchEvent(new ShowToastEvent({
title: 'Error', message: error.body?.message ?? 'Update failed', variant: 'error', mode: 'sticky'
}));
} finally {
this.isUpdating = false;
}
}
}
🛠️ Practical Mini-Implementation
Notice
refreshApex() is itself awaited — it returns a Promise too, resolving once the re-fetch completes. Awaiting it means the success toast only fires AFTER the fresh data has actually loaded, not while the refresh is still in flight.⚠️ Common Gotcha
Calling
refreshApex() BEFORE the imperative update completes (wrong order) would refresh with the OLD data still in the database, defeating the purpose entirely. Always sequence it: imperative action completes first, THEN refresh.Concept 4 of 7
Lightning Data Service Caching
Lightning Data Service (LDS) is the underlying engine behind standard adapters like
getRecord (Module 9, Concept 3). It maintains a SHARED, client-side cache — meaning if TWO different components on the same page both wire getRecord for the SAME record, Salesforce only fetches it ONCE and shares the cached result between them.⚡ Why This Matters
This shared caching is a genuine performance feature, not just an implementation detail — it reduces redundant server calls automatically, with zero extra code from you, whenever multiple components need the same record data.
TWO COMPONENTS, SAME RECORD, ONE SHARED FETCH
// componentA.js
@wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD] })
account;
// componentB.js — completely separate component, same page
@wire(getRecord, { recordId: '$recordId', fields: [INDUSTRY_FIELD] })
account;
// Salesforce's LDS cache recognizes these reference the SAME record
// and intelligently shares/merges the underlying cached data,
// rather than each component independently hitting the server
🛠️ Practical Mini-Implementation
This is one of the genuine reasons to PREFER standard adapters like
getRecord over a custom Apex method doing the same single-record lookup, when a standard adapter can do the job — you get this shared caching behavior for free, which a custom cacheable=true Apex wire (Module 9, Concept 6) does NOT automatically provide in the same way.⚠️ Common Gotcha
This shared caching applies specifically to LDS-managed standard adapters (
getRecord, getRecords, etc.) — it does NOT automatically apply to your own custom @AuraEnabled(cacheable=true) Apex methods, which use a different (still useful, but separate) platform caching mechanism covered in Concept 6.Concept 5 of 7
Why LDS Auto-Updates, Custom Apex Doesn't
Here's the genuinely interesting part: if Component A updates a record using LDS's
updateRecord function (or a standard component like lightning-record-form, which uses LDS internally), Component B's WIRED getRecord for that SAME record AUTOMATICALLY refreshes — no refreshApex() needed at all! This only works because BOTH sides are going through the same managed LDS cache.⚡ Why This Matters
This directly explains WHY Concept 1's stale data problem happens with custom Apex wires but often doesn't with standard LDS adapters — it's not random inconsistency, it's a precise architectural difference worth understanding deeply for interviews.
LDS AUTO-SYNC — no refreshApex needed
// componentA.js — updates via LDS's updateRecord
import { updateRecord } from 'lightning/uiRecordApi';
async handleSave() {
await updateRecord({
fields: { Id: this.recordId, Name: 'New Account Name' }
});
// NO refreshApex() call here — yet componentB still updates!
}
// componentB.js — completely separate component
@wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD] })
account;
// This automatically reflects the new Name — LDS's shared cache
// recognized the update and pushed it to every component watching this record
🛠️ Practical Mini-Implementation
This is a strong, practical reason to use
lightning/uiRecordApi's updateRecord (or standard components built on it) for simple record updates whenever possible — you get automatic cross-component data consistency for free, something a custom Apex DML method cannot provide on its own.⚠️ Common Gotcha
If Component A updates the SAME underlying data via a CUSTOM Apex method instead of LDS's
updateRecord (even if that Apex method does the exact same DML internally), Component B's wired getRecord will NOT automatically know about it — because the update didn't go through LDS's managed cache layer. The auto-sync benefit specifically requires using LDS's own update mechanism.Concept 6 of 7
cacheable=true Deep Dive
For your OWN
@AuraEnabled(cacheable=true) Apex methods (Module 9, Concept 6), Salesforce maintains a separate client-side cache, keyed by the method name AND its exact parameters. The platform decides when to serve cached data versus calling the server again, generally favoring cached data for quick repeated calls with identical parameters.⚡ Why This Matters
Understanding that caching is per-method-AND-parameters (not just per-method) explains why calling the same wired method with DIFFERENT parameters correctly fetches fresh data each time, while identical repeated parameter combinations may serve from cache.
CACHE KEY = METHOD + EXACT PARAMETERS
@wire(getOrdersForAccount, { accountId: '001AAA' }) // cache key: (getOrdersForAccount, accountId=001AAA)
ordersForA;
@wire(getOrdersForAccount, { accountId: '001BBB' }) // DIFFERENT cache key — different parameters
ordersForB;
// These are treated as entirely separate cache entries — no confusion between them
// But calling getOrdersForAccount with accountId='001AAA' AGAIN later
// may serve from the existing cache rather than hitting the server fresh
🛠️ Practical Mini-Implementation
This is exactly WHY
refreshApex() exists as a distinct, explicit tool (Concept 2) — it's the official way to tell the platform "I know you might have this cached, but I specifically need you to bypass that and fetch fresh data right now," for the exact scenario where you know the underlying data changed through a path the cache doesn't know about.⚠️ Common Gotcha — Module Wrap-Up
Don't assume
cacheable=true caching behaves identically across ALL contexts (different users, different sessions, Experience Cloud vs internal users) — exact cache duration and scope details can vary, and platform behavior here has evolved across releases. The RELIABLE, controllable fact to anchor on is: when you need guaranteed fresh data after a known change, refreshApex() is your explicit, dependable tool — don't rely on guessing cache timing.Concept 7 of 7
Complete Data Integration Strategy
Let's consolidate Modules 9, 10, and 11 into one confident, complete mental model for ANY data integration scenario you'll face.
⚡ Why This Matters
This is the kind of "tie it all together" synthesis question senior interviews specifically probe for — showing you understand how the pieces fit together, not just each piece in isolation.
THE COMPLETE DECISION TREE
// 1. Need to DISPLAY data, kept current automatically?
// → @wire (standard adapter if it fits, custom Apex if not)
// 2. Standard need (single record, list view, object metadata)?
// → Prefer standard adapters (getRecord, getListUi, getObjectInfo)
// → Get LDS's shared caching AND auto-cross-component-sync for free
// 3. Custom query logic needed (joins, complex filters)?
// → Custom Apex with @wire, remembering cacheable=true is required
// 4. One-time user-triggered ACTION (save, delete, search-on-click)?
// → Imperative Apex call, with proper try/catch/finally
// 5. Imperative action changes data a wire is displaying?
// → If updated via LDS's updateRecord: often auto-syncs, no extra step
// → If updated via custom Apex DML: call refreshApex() explicitly afterward
🛠️ Practical Mini-Implementation
Walking an interviewer through this exact decision tree, OUT LOUD, in response to "how would you design the data layer for this component" is a genuinely strong answer — it shows you're not just reciting syntax, but reasoning through trade-offs systematically.
⚠️ Common Gotcha — Phase 4 Wrap-Up
This module completes Phase 4 (Data Integration) — Modules 9 through 11 together form the complete toolkit for connecting components to real Salesforce data: declarative wiring, imperative actions, and keeping everything in sync. Phase 5 shifts to production-readiness: performance optimization, automated testing with Jest, security, deployment, and finally the capstone project bringing all 15 modules together.
💬 Module 11 Interview Questions (6)
Q1What exactly does refreshApex() need to be passed in order to work, and why does this requirement explain a specific way you must write your @wire call?
refreshApex() needs to be passed the full original wire result object (the same object the wire service itself manages internally), not just the extracted data property from that result. This means a wire call intended to later support refreshApex() must be written in function form, storing the entire incoming result parameter in a class property, rather than property form or a destructured version that only retains the data portion, since the full result object contains internal tracking information that refreshApex() relies on to correctly identify and re-trigger that specific wire.
"refreshApex() requires the full original wire result object, not just its extracted data, meaning the wire must be written in function form storing the entire result parameter, since that full object contains tracking information refreshApex() needs to correctly re-trigger the wire."
Q2A developer tries to force a wire to refresh by reassigning its reactive parameter to itself, such as writing this.recordId = this.recordId. Why is this not a reliable solution, and what should be used instead?
Reassigning a property to its own current value does not reliably guarantee the wire service will detect this as a meaningful change requiring a re-fetch, since the underlying value is technically unchanged, making this approach inconsistent and dependent on implementation details rather than a guaranteed, documented behavior. The reliable, officially supported solution is refreshApex(), which is specifically designed to explicitly force a wire result to re-fetch regardless of whether its reactive parameters have changed, making it the dependable choice for this exact scenario rather than relying on an unofficial workaround.
"Reassigning a reactive parameter to its own unchanged value does not reliably guarantee a re-fetch, since the value is technically unchanged; refreshApex() is the officially supported, reliable tool specifically designed to explicitly force a wire to re-fetch regardless of parameter changes."
Q3Two separate, unrelated components on the same Lightning page both wire getRecord for the same Account record. Does Salesforce fetch this record twice, once for each component? Explain why or why not.
No, Salesforce does not fetch the record twice in this scenario. Lightning Data Service, which powers standard adapters like getRecord, maintains a shared client-side cache recognized across components, so when multiple components wire the exact same record (and overlapping or compatible field sets), the underlying fetch happens once and the resulting cached data is shared and reused across all components referencing that same record, rather than each component independently triggering its own separate server request.
"No, Lightning Data Service maintains a shared client-side cache across components, so when multiple components wire the same record, the fetch happens once and the cached result is shared and reused rather than triggering separate independent server requests for each component."
Q4Component A updates a record using lightning/uiRecordApi's updateRecord function. Component B, an entirely separate component wiring getRecord for that same record, automatically reflects the change without any refreshApex() call. Why does this happen, and would the same automatic behavior occur if Component A instead used a custom Apex method to perform the update?
This automatic synchronization happens because both the update (via updateRecord) and the read (via the wired getRecord) are going through the same Lightning Data Service managed cache, which recognizes that the underlying record changed and proactively pushes that update to every component currently watching that record through LDS, without requiring any explicit refresh call. If Component A instead used a custom Apex method to perform the same underlying database update, this automatic synchronization would NOT occur, even if the Apex method performs identical DML, because that update path does not go through LDS's managed cache layer at all, meaning Component B's wire would remain unaware of the change and would require an explicit refreshApex() call to reflect the new data.
"Automatic synchronization occurs because both the update and the read go through Lightning Data Service's shared managed cache, which proactively notifies all components watching that record; a custom Apex method performing the same DML would NOT trigger this automatic sync, since it bypasses LDS entirely, requiring an explicit refreshApex() call instead."
Q5You call the same wired custom Apex method twice with two different sets of parameters. Are these treated as the same cache entry or different ones, and why does this matter for understanding when fresh data is fetched versus served from cache?
These are treated as entirely separate, distinct cache entries, since the platform's caching mechanism for cacheable Apex methods keys its cache based on the combination of the specific method being called AND the exact parameter values passed to it, not just the method name alone. This matters because it explains why calling the same wired method with genuinely different parameters correctly and reliably triggers a fresh server fetch each time (since each distinct parameter combination has no prior cache entry to draw from), while repeated calls with the exact same parameters may be served from the existing cached entry for that specific combination rather than hitting the server again.
"Different parameter values create entirely separate cache entries, since the cache key combines both the method name and its exact parameters, which explains why genuinely different parameter combinations reliably trigger fresh fetches while identical repeated parameter calls may be served from the existing cache entry."
Q6Walk through the complete decision process for designing the data layer of a component that displays a list of related records and includes a button to update one of those records' status.
First, determine the display need: since this requires ongoing, automatically-current display of a list of related records, this calls for @wire, and since it likely requires custom filtering logic (records related to a specific parent), a custom Apex method wired with cacheable=true is probably appropriate rather than a generic standard adapter. Second, address the update action: since clicking the status update button represents a one-time, user-triggered action with a side effect, this calls for an imperative Apex call, properly wrapped in try/catch/finally for robust error handling and loading state management. Third, address keeping the display in sync afterward: since the update goes through a custom Apex DML operation rather than Lightning Data Service's updateRecord, the wired list will not automatically reflect the change, so the full wire result object must be stored from the wire call specifically so that refreshApex() can be called immediately after the imperative update succeeds, ensuring the displayed list reflects the new status without requiring a full page reload.
"Use @wire with a custom Apex method for the ongoing filtered list display, use an imperative Apex call with proper try/catch/finally for the one-time status update action, and call refreshApex() on the stored full wire result immediately after the imperative update succeeds, since the custom Apex DML path will not automatically sync through Lightning Data Service."
📝 Module 11 Recap — Caching & Advanced Wire Patterns Mastered
✅ Wired data only re-fetches on reactive parameter changes — unrelated imperative changes leave it stale
✅ refreshApex() needs the FULL wire result object (function form), not just extracted data
✅ The complete pattern: imperative action completes, THEN refreshApex() is awaited
✅ Lightning Data Service shares a client-side cache across components wiring the same record
✅ Updates via LDS's updateRecord auto-sync across components; custom Apex DML does not
✅ cacheable=true caching keys on method + exact parameters — different parameters are different cache entries
✅ Complete decision tree: wire for display, imperative for actions, refreshApex() to bridge the gap between them
🎉 Phase 4 Complete — Data Integration Mastered!
Modules 9 through 11 gave you the complete data toolkit: declarative @wire adapters for reactive display, imperative Apex calls for on-demand actions, and the caching/refresh knowledge to keep everything in sync. Your components can now genuinely connect to real Salesforce data, not just hardcoded examples. Phase 5 is the home stretch — Performance Optimization, Jest Testing, Security & Deployment, and the full Capstone Project bringing all 15 modules together into one real application.
🎯 Before Moving to Module 12...
Try this: take the complete pattern from Concept 3 and deliberately test the bug from Concept 1 by removing the refreshApex() call — confirm the data goes stale, then add it back and confirm it's fixed. Then try the LDS auto-sync experiment from Concept 5 yourself, using lightning-record-form alongside a separate wired getRecord component, and watch the automatic sync happen with no refreshApex() needed at all. Module 12 begins Phase 5 with Performance Optimization — making sure everything you've built so far runs efficiently at real-world scale.
☕
☕ 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