Skip to main content

Mastering LWC Data: @wire vs Imperative Apex & refreshApex

๐Ÿ’ฌ In plain words: The @wire decorator is an automatic data subscription: it caches data, reacts to changes automatically, and serves as your default for reading information. Imperative Apex is a manual phone call: you decide exactly when to make it (like clicking a button or validating a step). Use refreshApex to update your wired data after an imperative action changes the underlying records.

๐Ÿ”‘ Key Points

  • @wire: Declarative, highly efficient, and reactive. Must be used with cacheable=true Apex methods.
  • Imperative Apex: Manual execution utilizing JavaScript Promises. Required for DML operations (inserts/updates/deletes) and conditional sequencing.
  • Reactivity: Prepending a parameter with $ (e.g., '$recordId') forces the wire to re-fire automatically whenever that variable changes.
  • Cache Refresh: refreshApex is required to update the UI after making data changes via imperative Apex, as the Lightning Data Service (LDS) cache does not automatically know a custom Apex update occurred.
๐ŸŽฌ Real-Life Example: The Missing Dollar Sign

Imagine a related-list component on an Account page that pulls Contacts via a wired Apex method. When users switch between Account tabs in a Console app, they complain that the old Account's Contacts remain stuck on the screen.

The Old/Bad Way: The developer passed the parameter as { accountId: recordId }. Because it lacked the dollar sign prefix, the wire captured the ID exactly once upon load and never re-fired. Thinking it was a caching bug, the developer incorrectly added a manual reload function inside renderedCallback, causing a dangerous infinite render loop.

The New/Good Way: Simply adding one character fixes everything: { accountId: '$recordId' }. The dollar sign marks the variable as reactive, telling the component to automatically re-fetch the data every time the tab changes.

๐Ÿ› ️ Core Concepts: Fetching & Refreshing

Salesforce provides two distinct ways to call Apex from Lightning Web Components. Understanding when to use which is the foundation of performant UI development.

  • @wire (The Reader): Binds a property or function directly to an Apex method. The method must be annotated with @AuraEnabled(cacheable=true). It participates fully in the Lightning Data Service (LDS) cache, returning { data, error }.
  • Imperative (The Actor): Called explicitly within an event handler or lifecycle hook. This is mandatory for DML, as DML methods cannot be cached. It utilizes standard JavaScript `try/catch` and `async/await` patterns.
  • refreshApex (The Bridge): If you use imperative Apex to update a record, the wired data on your screen will instantly become stale. You must call refreshApex(this.wiredResult) to force the wire to fetch the newest truth from the server.
import { LightningElement, api, wire } from 'lwc';
import getContacts from '@salesforce/apex/ContactCtrl.getContacts';
import updateContact from '@salesforce/apex/ContactCtrl.updateContact';
import { refreshApex } from '@salesforce/apex';

export default class ContactList extends LightningElement {
    @api recordId;
    _wiredContactsResult; // Variable to store the ENTIRE wire response

    // The '$' makes recordId reactive. It refires if the ID changes.
    @wire(getContacts, { accountId: '$recordId' })
    wiredContacts(result) {
        this._wiredContactsResult = result; // Keep WHOLE result for refreshApex
        if (result.data) {
            this.contacts = result.data;
        } else if (result.error) {
            this.error = result.error;
        }
    }

    async handleSave() {
        try {
            // Imperative call for DML (non-cacheable)
            await updateContact({ contact: this.editedContact }); 
            
            // Re-provision the wire to update the UI
            await refreshApex(this._wiredContactsResult); 
        } catch (error) {
            console.error('Save failed', error);
        }
    }
}
๐Ÿง  Core Takeaway: Wire reads, imperative acts. Use @wire for reactive reading. Use imperative Apex for DML. Always call refreshApex on the entire wired object after your own writes.
⚠ DEVELOPER TRAP: When using refreshApex, you must pass the entire provisioned object (the parameter that holds both data and error), not just the `.data` property. Passing this.contacts instead of this._wiredContactsResult will fail silently.

๐Ÿงญ 360 Card: @wire vs Imperative Apex

  • Rule: Use @wire as your default for all read-only data. Switch to imperative Apex only when you must control the exact timing of the execution.
  • Gain: Wires handle their own caching, re-firing, and component lifecycle management entirely behind the scenes with minimal code.
  • Reach for Imperative when: A user clicks a button, you need to sequence multiple calls sequentially, or you are executing DML (inserts, updates, deletes).
  • Limits: You cannot guarantee exactly when a wire will execute, and the first time it emits, its reactive variables (like recordId) may still be undefined. Always add null-checks in your wired functions.
  • Mirror (The UI API alternative): If you are updating a record via imperative Apex that is also tied to standard Lightning components, use notifyRecordUpdateAvailable instead of refreshApex to update the global LDS cache.

๐Ÿ’ก Core Q&A

Q: When do you use @wire vs an imperative Apex call, and how do you refresh the data afterward?
๐ŸŽฏ Say this first: Use @wire as your default for cached, reactive reads. Use imperative Apex when you need to trigger an action (like a button click or DML). To update the screen after an imperative change, call refreshApex on the stored wire result.

Here is the standard operating procedure: Use `@wire` anytime you need to display a related list or read-only summary on load. Use imperative Apex when a user initiates a transaction. Because imperative DML bypasses the LDS cache, your wired properties won't automatically know the database was updated. By capturing the complete wired result in a variable (e.g., `this.wiredResult`) and passing it to `refreshApex(this.wiredResult)` after your imperative call succeeds, you force the component to fetch the latest server data.

Q: Your @wire method isn't refiring when the recordId changes on the page. What went wrong?

Almost certainly, the reactive $ prefix is missing. A parameter only reacts and triggers a new server call when written as a string with a dollar sign (e.g., '$recordId'). If you pass it bare (e.g., this.recordId), it evaluates exactly once during the initial load and becomes permanently stagnant.

Q: A cacheable @wire is showing stale data because a background integration updated the Salesforce record. How do you fix this?

The Lightning Data Service (and your wire cache) is completely blind to server-side changes made by integrations, flows, or other users. It will never auto-refresh on its own in these scenarios. You have three primary solutions:

  • Manual Push: Provide a manual "Refresh" button that calls refreshApex.
  • Event-Driven (Best Practice): Subscribe to a Platform Event or Change Data Capture (CDC) via the lightning/empApi module. When the event arrives confirming the integration finished, trigger refreshApex programmatically for true push freshness.
  • Acceptable Staleness: If real-time accuracy isn't critical, simply display a "Last Updated" timestamp and accept bounded staleness.