@wire vs Imperative Apex in LWC

Getting data from Apex into LWC components requires either the @wire decorator or imperative Apex calls. This post explains when to use each approach and how to balance reactive data with manual control.

1. Understanding @wire

@wire is a reactive data service that automatically fetches data and updates your component when inputs change.

Simple @wire Example


// accountList.js
import { LightningElement, wire, track } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountList extends LightningElement {
    @track error;

    @wire(getAccounts)
    wiredAccounts({ error, data }) {
        if (data) {
            this.accounts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.accounts = undefined;
        }
    }
}
  

<!-- accountList.html -->
<template>
    <div if:true={accounts}>
        <template for:each={accounts} for:item="account">
            <div key={account.Id}>
                <p>{account.Name}</p>
            </div>
        </template>
    </div>

    <lightning-spinner if:true={loading}></lightning-spinner>

    <div if:true={error}>
        Error: {error.body.message}
    </div>
</template>
  

2. @wire with Reactive Parameters

The power of @wire is automatic re-fetching when parameters change:


// accountDetails.js
import { LightningElement, wire, track } from 'lwc';
import getAccountById from '@salesforce/apex/AccountController.getAccountById';

export default class AccountDetails extends LightningElement {
    @track selectedId = '001D3A20D50B8D5A9000000';
    accounts;
    error;

    // When selectedId changes, @wire automatically refetches
    @wire(getAccountById, { accountId: '$selectedId' })
    wiredAccount({ error, data }) {
        if (data) {
            this.accounts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.accounts = undefined;
        }
    }

    handleSelectChange(event) {
        this.selectedId = event.target.value;  // Triggers re-fetch
    }
}
  

Apex Controller


// AccountController.cls
public with sharing class AccountController {
    
    @AuraEnabled(cacheable=true)
    public static Account getAccountById(String accountId) {
        return [SELECT Id, Name, Phone, Website FROM Account WHERE Id = :accountId];
    }
}
  

3. Imperative Apex Calls

Imperative calls give you more control than @wire. Call Apex methods manually whenever you need.

Basic Imperative Call


// accountForm.js
import { LightningElement, track } from 'lwc';
import createAccount from '@salesforce/apex/AccountController.createAccount';

export default class AccountForm extends LightningElement {
    @track accountName = '';
    @track isLoading = false;
    @track error;

    async handleSaveAccount() {
        this.isLoading = true;
        this.error = undefined;

        try {
            const result = await createAccount({ 
                accountName: this.accountName 
            });
            
            console.log('Account created:', result);
            this.accountName = '';  // Clear form
        } catch (error) {
            this.error = error.body.message;
        } finally {
            this.isLoading = false;
        }
    }
}
  

Apex Method for Imperative Call


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

4. Comparison: @wire vs Imperative

Feature @wire Imperative
Auto data refresh ✅ Yes (on param change) ❌ Manual control
Caching support ✅ Built-in (with cacheable=true) ❌ Manual caching
User interaction ❌ Limited ✅ Full control
Data mutations ❌ Read-only ✅ Create/Update/Delete
Error handling ✅ Automatic ✅ Try/catch

5. When to Use @wire

✅ Good Use Cases for @wire


// Product filter component
import { LightningElement, wire, track } from 'lwc';
import getProducts from '@salesforce/apex/ProductController.getProducts';

export default class ProductFilter extends LightningElement {
    @track category = 'Electronics';
    @track minPrice = 0;

    @wire(getProducts, { 
        category: '$category',
        minPrice: '$minPrice'
    })
    products;

    handleCategoryChange(event) {
        this.category = event.target.value;  // Auto-refetch
    }
}
  

6. When to Use Imperative Apex

✅ Good Use Cases for Imperative


// Account with bulk operations
import { LightningElement, track } from 'lwc';
import updateAccountBatch from '@salesforce/apex/AccountController.updateAccountBatch';

export default class BulkAccountUpdater extends LightningElement {
    @track selectedAccounts = [];
    @track updating = false;
    @track progress = 0;

    async handleBulkUpdate() {
        this.updating = true;

        try {
            const result = await updateAccountBatch({ 
                accountIds: this.selectedAccounts 
            });
            
            console.log(`Updated ${result.updated} accounts`);
        } catch (error) {
            console.error('Bulk update failed:', error);
        } finally {
            this.updating = false;
        }
    }
}
  

7. Combined Approach

Often the best solution combines both @wire and imperative:


// accountManager.js
import { LightningElement, wire, track } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
import updateAccount from '@salesforce/apex/AccountController.updateAccount';

export default class AccountManager extends LightningElement {
    @track accounts = [];
    @track error;
    @track updating = false;

    // Display list with @wire
    @wire(getAccounts)
    wiredAccounts({ error, data }) {
        if (data) {
            this.accounts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
        }
    }

    // Update with imperative call
    async handleUpdateAccount(accountId, newValues) {
        this.updating = true;

        try {
            await updateAccount({ 
                accountId, 
                accountData: newValues 
            });

            // Refresh the @wire data
            this.refreshAccounts();
        } catch (error) {
            this.error = error;
        } finally {
            this.updating = false;
        }
    }

    refreshAccounts() {
        // Force refresh the wired data
        refreshApex(this.wiredAccounts);
    }
}
  

8. Best Practices

For @wire:

For Imperative:

Key Takeaways