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.
@wire is a reactive data service that automatically fetches data and updates your component when inputs change.
// 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>
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
}
}
// 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];
}
}
Imperative calls give you more control than @wire. Call Apex methods manually whenever you need.
// 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;
}
}
}
// AccountController.cls
public with sharing class AccountController {
@AuraEnabled
public static Account createAccount(String accountName) {
Account acc = new Account(Name = accountName);
insert acc;
return acc;
}
}
| 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 |
// 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
}
}
// 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;
}
}
}
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);
}
}
cacheable=true for improved performance$propertyName for reactive parameterscacheable=true