Wire or imperative: calling Apex from a Lightning Web Component
Three ways to get data into a component, and how to pick between them — starting with the one that needs no Apex at all.
A Lightning Web Component can reach Salesforce data through Lightning Data Service,
through an Apex method bound with @wire, or through an Apex method called
imperatively. All three work, which is exactly why components end up using the wrong
one. The choice decides who controls timing, whether results are cached, and how much
code you have to write and test.
First question: do you need Apex at all?
To show, create or edit a record, usually not. Lightning Data Service (LDS) covers it
with base components such as lightning-record-form, and with the
getRecord wire adapter and the createRecord,
updateRecord and deleteRecord functions from
lightning/uiRecordApi.
What you get without writing a line of Apex:
- Sharing, object permissions and field-level security enforced for the current user.
- One client-side cache shared by every component on the page, so a record edited in one place updates everywhere it is shown.
- No Apex class to maintain, and no test class to write for it.
Reach for Apex when LDS cannot express the request: a filtered list of related records, an aggregate, several objects changed in one transaction, or business logic that must run on the server.
The wire service: reads that keep themselves current
@wire is declarative. You state which method and which parameters; the
framework decides when to call it, and calls it again when a parameter changes. The Apex
method has to be marked cacheable:
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContacts(Id accountId) {
return [
SELECT Id, Name, Email
FROM Contact
WHERE AccountId = :accountId
WITH USER_MODE
ORDER BY Name
LIMIT 50
];
}
}
import { LightningElement, api, wire } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
export default class ContactList extends LightningElement {
@api recordId;
@wire(getContacts, { accountId: '$recordId' })
contacts;
}
The $ prefix makes recordId reactive: when it changes, the
wire fetches again. The property receives an object with data and
error, and the template renders whichever one is present:
<template lwc:if={contacts.data}>
<template for:each={contacts.data} for:item="contact">
<p key={contact.Id}>{contact.Name}</p>
</template>
</template>
<template lwc:elseif={contacts.error}>
<p>Could not load contacts.</p>
</template>
Three properties of the wire service are worth knowing before relying on it:
- You do not control when it runs. There is no way to say “call it now”. If timing matters, the wire is the wrong tool.
- Results are cached on the client. That is what makes it fast, and also why a wired list can show yesterday's answer after the data changed on the server.
- Cacheable methods cannot change data. The platform blocks DML inside them, so a wire is for reads only.
Imperative calls: when you decide the moment
Importing an Apex method gives you a function that returns a promise. Calling it yourself is the right choice when the call changes data, or when it has to happen on a user action such as a button click rather than on load.
@AuraEnabled
public static void removeContact(Id contactId) {
Contact toRemove = new Contact(Id = contactId);
delete as user toRemove;
}
import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import removeContact from '@salesforce/apex/ContactController.removeContact';
export default class ContactList extends LightningElement {
@api recordId;
@wire(getContacts, { accountId: '$recordId' })
contacts;
async handleRemove(event) {
try {
await removeContact({ contactId: event.target.dataset.id });
await refreshApex(this.contacts);
} catch (error) {
this.dispatchEvent(new ShowToastEvent({
title: 'Could not remove contact',
message: error.body?.message ?? 'Unexpected error',
variant: 'error'
}));
}
}
}
Note the second line of the handler. The wired list knows nothing about the delete, so
its cache is now stale; refreshApex tells the framework to fetch it again.
It takes the whole value the wire provisioned — this.contacts, not
this.contacts.data.
Choosing between them
| You need to | Use |
|---|---|
| Show or edit one record | Lightning Data Service, no Apex |
| Read data that should reload when an input changes | @wire with a cacheable method |
| Create, update or delete through custom logic | Imperative call |
| Run something on a click, or in a fixed order | Imperative call |
| Read data that must never come from a cache | Imperative call to a non-cacheable method |
Mistakes that keep coming up
-
Mutating wired data. Objects provisioned by a wire are read-only.
Sorting the array in place or assigning to a field throws. Copy first:
[...this.contacts.data]. -
Forgetting the refresh. After an imperative write, every wire that
showed the old data still shows it until
refreshApexis called. For records displayed through LDS,notifyRecordUpdateAvailabledoes the same job. -
Trusting the client. An
@AuraEnabledmethod is an endpoint that any user with access to the class can call with any parameters. Declare the classwith sharing, run queries and DML in user mode, and validate input on the server as if the component did not exist. -
Swallowing errors. A rejected promise with no
catchleaves the user looking at a button that did nothing. Throw anAuraHandledExceptionwith a readable message in Apex, and show it.
In short
Start with Lightning Data Service. Use @wire for reads you want the
framework to keep current. Call Apex imperatively when you are changing data or need to
choose the moment — and refresh whatever that change made stale.