The answer, briefly: use @wire when the component displays data and the framework should manage it, and an imperative call when the user’s action decides when the call happens. The rule that decides most cases is not style, it is refreshability: only a wired result can be handed to refreshApex. If the value on screen has to change after a save somewhere else on the page, it has to be wired.
The mechanical difference
A wired Apex method is called by the framework, on its schedule, whenever its reactive parameters change. You do not decide the timing and you cannot await it.
An imperative call is a promise you invoke yourself. You choose the moment, you can await it, and you can catch its failure in the same place you started it.
Both need @AuraEnabled. Only a wired method needs cacheable=true, and that keyword carries a condition: a cacheable method must not change anything. Try to wire a method that performs DML and it will not work, which is the platform stopping you from writing a component whose display logic silently mutates records.
public with sharing class AccountHealthController {
// Wired: read-only, cacheable, refreshable.
@AuraEnabled(cacheable=true)
public static Decimal getOpenOpportunityTotal(Id accountId) {
AggregateResult result = (AggregateResult) [
SELECT SUM(Amount) total
FROM Opportunity
WHERE AccountId = :accountId AND IsClosed = false
];
Decimal total = (Decimal) result.get('total');
return total == null ? 0 : total;
}
}
Keep the wire result, or you cannot refresh it
This is the part that decides the design, and it is easy to get wrong by writing the tidier-looking version first.
Wiring straight to a property gives you the value and nothing else:
// Tidy, and a dead end.
@wire(getOpenOpportunityTotal, { accountId: '$recordId' })
openTotal;
Wiring to a function lets you hold the whole result object, which is what refreshApex needs:
wiredTotal;
@wire(getOpenOpportunityTotal, { accountId: '$recordId' })
wiredOpenTotal(result) {
this.wiredTotal = result;
}
handleRefresh() {
return refreshApex(this.wiredTotal);
}
An imperative call has no equivalent. There is nothing to refresh, because there is no cached wire entry — you simply call it again. That is fine when the user pressed a button, and no help at all when something else on the page saved a record and your number is now wrong.
The reactive parameter is the whole point of $
The $recordId syntax means “re-run when this changes”. Without the dollar the value is read once, at construction, and never again, so a component that works on a record page and breaks in a flow screen or a modal is usually a missing dollar sign.
One consequence people trip over: while a reactive parameter is undefined, the wire does not call Apex at all. On first render recordId can be undefined for a tick, which is why a wire handler that assumes data exists throws Cannot read properties of undefined on load and then works fine. Handle the empty state. It is not a race you can win.
Lightning Data Service first, Apex second

Before writing either, ask whether Apex is needed. For fields on one record, getRecord from lightning/uiRecordApi costs no Apex, respects field-level security automatically, and shares its cache with everything else on the page asking for the same fields.
Apex earns its place when the shape of the data is not a record: an aggregate, a cross-object rollup, a calculation, anything needing more than one query. The component this article is drawn from does both — LDS for the account’s own fields, and one cacheable Apex method for a pipeline total that LDS cannot express.
How to choose, in one pass
- Displaying fields from one record? Lightning Data Service. No Apex.
- Displaying derived or aggregated data that must survive a save elsewhere? Wired Apex, wired to a function, refreshed with
refreshApex. - Acting on a click: save, submit, recalculate, call out? Imperative. It is not cacheable, so it cannot be wired anyway.
- Need to await a result, or sequence two calls? Imperative.
- Search-as-you-type? Imperative, debounced. A wire that fires on every keystroke is a governor limit with a UI.
What I would do differently having got it wrong
I have written the tidy wired-property version and then had to rewrite the component when a related-list save left the number stale. Wiring to a function costs three extra lines and keeps the option open, so I now do it by default and only simplify if it is genuinely display-only forever.
The other habit worth having: do not reach for imperative because it feels more controllable. Control is what the platform is taking off your hands, and the wire’s caching is a real performance feature. Two components asking for the same wired data make one call.
Frequently asked
Can I wire a method that inserts a record? No. Wiring requires cacheable=true, and a cacheable method may not mutate data. That restriction is the platform preventing an accidental write on every render.
Does refreshApex refresh Lightning Data Service too? No. refreshApex refreshes wired Apex results; notifyRecordUpdateAvailable is the LDS equivalent. Mixing the two up produces a component where half the panel updates.
Is an imperative call slower? Per call, no. Across a page, often yes, because you lose the shared cache. Two components calling imperatively make two round trips where two wires would have made one.
How do I test the wire in Jest? Emit through the wire adapter’s test utility rather than mocking the Apex module, so the reactive behaviour is what you are testing. That deserves its own article, and it is on the list.
