Calling Apex Methods in LWC

Learn the 3 approaches to call Apex methods in Lightning Web Components — wire a property, wire a function, and call imperatively.

1. Introduction

Lightning Web Components (LWC) provide a modern framework for building user interfaces on the Salesforce platform. Interacting with server-side logic via Apex is a fundamental aspect of LWC development.

These are the 3 approaches to call Apex Methods in LWC:
Wire a property
Wire a function
Call a method imperatively

2. Importing Apex Methods

Before you can use an Apex method in LWC, you must import it. This is done by the following syntax:

import apexMethodName from '@salesforce/apex/ApexClassName.methodName';

apexMethodName — This is the local name you'll use to refer to the Apex method in your LWC JavaScript. It can be the same as the Apex method name or a different name.
@salesforce/apex/ApexClassName.methodName — This is the import path.
ApexClassName — The name of your Apex class.
methodName — The name of the Apex method you want to call.

3. Apex Controller

The Apex methods used for both examples (wire as property and wire as function) are defined below. Note that methods must be annotated with @AuraEnabled to be accessible from LWC. Use cacheable=true for read-only methods to enable caching.

public with sharing class AccountController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccountList() {
        return [SELECT Id, Name FROM Account LIMIT 10];
    }
}

public with sharing class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> getContactsByAccountId(Id accountId) {
        return [SELECT Id, FirstName, LastName FROM Contact WHERE AccountId = :accountId];
    }
}

@AuraEnabled — Makes the method accessible from LWC.
cacheable=true — Enables client-side caching for read-only queries. Required when using @wire.
• For DML operations (insert, update, delete), do not use cacheable=true.

4. Approach 1 — @wire as a Property

This is the simpler approach, ideal for basic data retrieval. You decorate a property with @wire, and the result of the Apex method call is automatically assigned to that property.

Example: Displaying a list of Accounts.

import { LightningElement, wire } from 'lwc';
import getAccountList from '@salesforce/apex/AccountController.getAccountList';

export default class AccountList extends LightningElement {
    @wire(getAccountList)
    accounts;

    get hasAccounts() {
        return this.accounts.data && this.accounts.data.length > 0;
    }

    get error() {
        return this.accounts.error;
    }
}
<template>
    <template if:true={hasAccounts}>
        <ul>
            <template for:each={accounts.data} for:item="account">
                <li key={account.Id}>{account.Name}</li>
            </template>
        </ul>
    </template>
    <template if:true={error}>
        <div class="error">{error.body.message}</div>
    </template>
    <template if:true={!hasAccounts && !error}>
        No Accounts found
    </template>
</template>

When to Use:
• Displaying data in a read-only context.
• When performance is critical and data is cacheable.
• When rapid development is a priority.

5. Approach 2 — @wire as a Function

This approach provides greater control over how the data is handled. You decorate a function with @wire, and this function is called with an object containing data and error properties.

Example: Displaying Contacts related to an Account.

import { LightningElement, wire, api } from 'lwc';
import getContactsByAccountId from '@salesforce/apex/ContactController.getContactsByAccountId';

export default class ContactList extends LightningElement {
    @api recordId; // Account Id passed from parent component or record page
    contacts;
    error;
    isLoading = true;

    @wire(getContactsByAccountId, { accountId: '$recordId' })
    wiredContacts({ error, data }) {
        this.isLoading = true; // Set loading to true before the apex call returns
        if (data) {
            this.contacts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.contacts = undefined;
            console.error(error);
        }
        this.isLoading = false; // Set loading to false after the apex call returns
    }
}
<template>
    <template if:true={isLoading}>
        <lightning-spinner alternative-text="Loading Contacts..."></lightning-spinner>
    </template>
    <template if:true={contacts}>
        <ul>
            <template for:each={contacts} for:item="contact">
                <li key={contact.Id}>{contact.FirstName} {contact.LastName}</li>
            </template>
        </ul>
    </template>
    <template if:true={error}>
        <div class="error">{error.body.message}</div>
    </template>
</template>

When to Use:
• When you need custom logic on retrieved data.
• When you need detailed error handling.
• When you need to manage loading states explicitly.

6. Approach 3 — Calling Imperatively

Imperative calls provide more direct control over the Apex method invocation. This is useful for performing actions (like creating or updating records) or when you need fine-grained control over error handling.

import { LightningElement } from 'lwc';
import createAccount from '@salesforce/apex/AccountController.createAccount';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class CreateAccount extends LightningElement {
    accountName = '';
    isLoading = false;

    handleNameChange(event) {
        this.accountName = event.target.value;
    }

    createAccountHandler() {
        this.isLoading = true;
        createAccount({ accountName: this.accountName })
            .then(result => {
                this.isLoading = false;
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Success',
                        message: 'Account created!',
                        variant: 'success'
                    })
                );
                this.accountName = ''; // Clear input
            })
            .catch(error => {
                this.isLoading = false;
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Error creating record',
                        message: error.body.message,
                        variant: 'error'
                    })
                );
            });
    }
}
<template>
    <template if:true={isLoading}>
        <lightning-spinner alternative-text="Creating Account..."></lightning-spinner>
    </template>
    <lightning-input type="text" label="Account Name" onchange={handleNameChange}></lightning-input>
    <lightning-button label="Create Account" onclick={createAccountHandler}></lightning-button>
</template>

Apex Controller for Imperative Call:

public with sharing class AccountController {
    @AuraEnabled
    public static Account createAccount(String accountName) {
        Account acc = new Account(Name = accountName);
        insert acc;
        return acc;
    }
}

When to Use:
• Performing DML operations (insert, update, delete).
• When you need to handle complex logic or error scenarios.
• When caching is not beneficial.

7. Comparison — Which Approach to Use?

@wire as Property:
• Simplest syntax — ideal for read-only, cacheable data.
• Less control over data handling and loading states.
• Best for: displaying lists, records, metadata.

@wire as Function:
• More control — handle data, errors, and loading states separately.
• Still declarative — auto-fires when parameters change.
• Best for: complex data display with loading spinners and error messages.

Imperative Call:
• Full control — call the method on demand (e.g., on button click).
• Must use .then() and .catch() for async handling.
• Best for: DML operations, form submissions, user-triggered actions.

Popular posts from this blog

Handling Errors in LWC

Hello World using LWC

Lightning Card in LWC