LWC gives you two ways to call Apex from a component: the @wire decorator and imperative invocation. They look similar at first glance, but they solve very different problems.
The reactive way — @wire
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> findByAccount(Id accountId) {
return [SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId];
}
}
import { LightningElement, api, wire } from 'lwc';
import findByAccount from '@salesforce/apex/ContactController.findByAccount';
export default class ContactList extends LightningElement {
@api accountId;
@wire(findByAccount, { accountId: '$accountId' })
contacts; // { data, error }
get rows() { return this.contacts?.data ?? []; }
get error() { return this.contacts?.error; }
}
What you get:
- The wire fires automatically on first render.
- It re-fires whenever
$accountIdchanges (the$makes it reactive). - The result is cached by the Lightning Data Service, so two components asking for the same data share one network round-trip.
- The Apex method must be
@AuraEnabled(cacheable=true)— the cache contract requires the method to be free of side effects.
The on-demand way — imperative
public with sharing class ContactController {
@AuraEnabled
public static Contact upsertContact(Contact c) {
upsert c;
return c;
}
}
import { LightningElement } from 'lwc';
import upsertContact from '@salesforce/apex/ContactController.upsertContact';
export default class ContactForm extends LightningElement {
contact = { FirstName: '', LastName: '', Email: '' };
isSaving = false;
async handleSave() {
this.isSaving = true;
try {
const saved = await upsertContact({ c: this.contact });
this.dispatchEvent(new CustomEvent('saved', { detail: saved }));
} catch (err) {
this.error = err.body?.message ?? 'Save failed';
} finally {
this.isSaving = false;
}
}
}
What you get:
- Full control over when the call happens — on click, on timer, on validation pass.
- A returned
Promiseyou canawaitandtry/catch. - The ability to call non-cacheable methods, which is mandatory for any DML.
Side-by-side
| @wire | Imperative | |
|---|---|---|
| When does it fire? | Framework decides (initial + reactive deps) | You decide |
| Apex annotation | @AuraEnabled(cacheable=true) required | @AuraEnabled (cacheable optional) |
| Side effects allowed? | No — must be read-only | Yes — DML, callouts, anything |
| Caching | Automatic via Lightning Data Service | None (write your own if needed) |
| Refresh API | refreshApex(wiredVar) | Just call the function again |
| Error handling | Inspect .error property | try/catch or .catch() |
| Reactive parameters | Yes ('$prop') | No — pass values explicitly |
When to choose which
Use @wire when:
- You need to read data on render or in reaction to parameter changes.
- You want caching and refresh with minimal code.
- The data is genuinely side-effect-free (queries, describe calls, custom-metadata reads).
Use imperative when:
- You’re performing CRUD — insert, update, delete.
- The call must be conditional (only fire after a user action).
- You need error handling control beyond inspecting an
.errorproperty. - You want to chain calls (
await getA(); await getB(prevResult)).
Refreshing wired data after an imperative write
A classic interview question. The pattern:
import { refreshApex } from '@salesforce/apex';
@wire(findByAccount, { accountId: '$accountId' })
wiredContacts; // bind to a variable so we can refresh it
async handleSave() {
await upsertContact({ c: this.contact });
await refreshApex(this.wiredContacts); // re-fetch the wire's data
}
refreshApex invalidates the LDS cache entry and re-runs the wire. Without it, your wire will keep serving stale cached data after a write.
Verified against: LWC Developer Guide — Call Apex Methods, Wire Service. Last reviewed 2026-05-17 for Spring ‘26 release.