← All articles

Lightning Web Components · Intermediate · For developers · 5 min read

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 toUse
Show or edit one recordLightning Data Service, no Apex
Read data that should reload when an input changes@wire with a cacheable method
Create, update or delete through custom logicImperative call
Run something on a click, or in a fixed orderImperative call
Read data that must never come from a cacheImperative 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 refreshApex is called. For records displayed through LDS, notifyRecordUpdateAvailable does the same job.
  • Trusting the client. An @AuraEnabled method is an endpoint that any user with access to the class can call with any parameters. Declare the class with 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 catch leaves the user looking at a button that did nothing. Throw an AuraHandledException with 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.

Building on Lightning Web Components?

Tell us what you are building on Salesforce and we will get back to you. The first call is free, and you will get a straight answer on whether we are the right team for the job.

contactus@trailteam.dev