Different ways to Call Apex Methods in LWC

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. This article provides a comprehensive guide to the methods available for calling Apex from your LWC components, complete with practical examples.

Importing Apex Methods:

Before you can use an Apex method in your LWC, you must import it. This is done using 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.

Examples:

import getAccountList from '@salesforce/apex/AccountController.getAccountList';
import createAccount from '@salesforce/apex/AccountController.createAccount';
import getContactsByAccountId from '@salesforce/apex/ContactController.getContactsByAccountId';

Different ways to Call Apex Methods in LWC

To interact with Salesforce data and business logic, Lightning Web Components (LWC) can call Apex methods. After importing an Apex class method, you can call it either via the wire service or imperatively. These are the below Approaches to Call Apex Methods

  • Wire a property
  • Wire a function
  • Call a method imperatively

Calling Apex Methods Declaratively with @wire

The @wire decorator offers a declarative approach to fetching data from Apex. It simplifies data retrieval and automatically handles many aspects of the server-side communication. There are two ways to use @wire: as a property and as a function.

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;
    }
} 
// Call Apex Methods -flutterant.com
<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> 
// Call Apex Methods -flutterant.com

Advantages:

  • Simplified Syntax: Minimal code.
  • Automatic Data Handling: Handles Apex call and assigns the result.
  • Built-in Error Handling (Basic): Provides basic error information.
  • Performance Benefits (with cacheable=true): Caching improves performance.

When to Use:

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

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 based on the Account Id.

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
    }
}
// Call Apex Methods -flutterant.com
<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>

Advantages:

  • Fine-Grained Control: Custom data processing and error handling.
  • Parameter Passing: Enables passing parameters to the Apex method.
  • Handling Loading State: Easily manage loading states.

When to Use:

  • When you need to pass parameters to the Apex method.
  • When you need custom logic on retrieved data.
  • When you need detailed error handling.
  • When you need to manage loading states explicitly.

Apex Controller (Used for both examples above):

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];
    }
}
// Call Apex Methods -flutterant.com

3. Calling Apex Methods 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.

Example: Creating a new Account.

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'
                    })
                );
            });
    }
}
// Call Apex Methods -flutterant.com
<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> 
// Call Apex Methods -flutterant.com

Apex Controller:

public with sharing class AccountController {
    @AuraEnabled
    public static Account createAccount(String accountName) {
        Account acc = new Account(Name = accountName);
        insert acc;
        return acc;
    }
} 
// Call Apex Methods -flutterant.com

Advantages:

  • Full Control: Complete control over the Apex call.
  • Performing Actions: Ideal for data modification.
  • Complex Logic and Error Handling: Allows for complex logic and detailed error handling.
  • Chaining Apex Calls: Enables chaining multiple Apex calls.

When to Use:

  • Performing DML operations.
  • When you need to handle complex logic or error scenarios.
  • When you need to chain multiple Apex calls.
  • When caching is not beneficial.
  • When you need to refresh data after an action.

Benefits of Imperative Calls:

  • Flexibility: Greatest flexibility in interacting with Apex.
  • Real-time Updates: Immediate UI updates after an action.
  • Customizable User Experience: Tailored user experience with custom messages.

Conclusion

LWC offers two main ways to call Apex: the declarative @wire and imperative calls. @wire simplifies data retrieval and is best for displaying data, especially with caching. Imperative calls provide greater control for actions and complex scenarios. Choosing the right approach depends on your component’s needs

For more details please do follow Salesforce.help. Kindly share to your Salesforce friends. Happy Coding 🙂

Author

  • Satyam parasa

    Satyam Parasa is a Salesforce and Mobile application developer. Passionate about learning new technologies, he is the founder of Flutterant.com, where he shares his knowledge and insights.

    View all posts

Leave a Comment