Wire Service & Apex Integration
📖 Concept
The Wire Service is LWC's reactive data binding mechanism. It connects your component to Salesforce data sources — either Lightning Data Service (LDS) adapters or custom Apex methods.
Wire Service fundamentals:
- Uses the
@wiredecorator to bind to a data source - Automatically re-fetches data when input parameters change (reactive binding with
$prefix) - Caches data client-side (LDS cache) for performance
- Returns
{ data, error }— always check both
Two ways to call Apex from LWC:
Wire (reactive) — Automatic, declarative
- Data fetches automatically when inputs change
- Results are cached and shared across components
- Cannot pass complex objects as parameters
- Best for: data display, read operations
Imperative (manual) — Explicit, controlled
- You call the method manually (on button click, etc.)
- You handle the Promise (then/catch or async/await)
- Can pass complex objects
- Best for: DML operations, conditional fetching, user-triggered actions
Apex method requirements for LWC:
- Must be
@AuraEnabled(not@RemoteAction) - For wire: must be
@AuraEnabled(cacheable=true)— read-only, no DML - Must be
static - Parameters must match wire/imperative call exactly
Lightning Data Service (LDS): Built-in wire adapters that don't require custom Apex:
getRecord— Read a single recordgetRecords— Read multiple recordscreateRecord— Create a recordupdateRecord— Update a recorddeleteRecord— Delete a recordgetObjectInfo— Get object metadatagetPicklistValues— Get picklist options
💻 Code Example
1// Wire Service & Apex Integration23// === APEX CONTROLLER ===4// AccountController.cls5public with sharing class AccountController {67 // Cacheable — for @wire (read-only, no DML)8 @AuraEnabled(cacheable=true)9 public static List<Account> getAccounts(String searchTerm) {10 String searchKey = '%' + searchTerm + '%';11 return [12 SELECT Id, Name, Industry, AnnualRevenue, CreatedDate13 FROM Account14 WHERE Name LIKE :searchKey15 ORDER BY Name16 LIMIT 2017 ];18 }1920 @AuraEnabled(cacheable=true)21 public static List<Contact> getContactsByAccount(Id accountId) {22 return [23 SELECT Id, FirstName, LastName, Email, Phone, Title24 FROM Contact25 WHERE AccountId = :accountId26 ORDER BY LastName27 ];28 }2930 // Non-cacheable — for imperative calls (DML operations)31 @AuraEnabled32 public static Account createAccount(String name, String industry) {33 Account acc = new Account(Name = name, Industry = industry);34 insert acc;35 return acc;36 }3738 @AuraEnabled39 public static void updateAccountIndustry(Id accountId, String industry) {40 Account acc = new Account(Id = accountId, Industry = industry);41 update acc;42 }43}4445// === LWC — WIRE SERVICE ===46// accountSearch.js47import { LightningElement, wire, track } from 'lwc';48import getAccounts from '@salesforce/apex/AccountController.getAccounts';49import getContactsByAccount from '@salesforce/apex/AccountController.getContactsByAccount';50import createAccount from '@salesforce/apex/AccountController.createAccount';51import { refreshApex } from '@salesforce/apex';52import { ShowToastEvent } from 'lightning/platformShowToastEvent';5354export default class AccountSearch extends LightningElement {55 searchTerm = '';56 selectedAccountId;5758 // === WIRE — Reactive data fetching ===5960 // Wire to Apex method (re-queries when searchTerm changes)61 @wire(getAccounts, { searchTerm: '$searchTerm' })62 wiredAccounts; // { data: [...], error: {...} }6364 // Wire with hold reference for refresh65 _wiredContactsResult;66 contacts;67 contactsError;6869 @wire(getContactsByAccount, { accountId: '$selectedAccountId' })70 wiredContacts(result) {71 this._wiredContactsResult = result; // Hold ref for refreshApex72 const { data, error } = result;73 if (data) {74 this.contacts = data;75 this.contactsError = undefined;76 } else if (error) {77 this.contactsError = error.body.message;78 this.contacts = undefined;79 }80 }8182 // Getters83 get accounts() {84 return this.wiredAccounts.data || [];85 }8687 get hasAccounts() {88 return this.accounts.length > 0;89 }9091 get isLoading() {92 return !this.wiredAccounts.data && !this.wiredAccounts.error;93 }9495 // === EVENT HANDLERS ===9697 handleSearchChange(event) {98 // Debounce search input99 clearTimeout(this._debounceTimer);100 this._debounceTimer = setTimeout(() => {101 this.searchTerm = event.target.value;102 // Wire automatically re-fetches when searchTerm changes103 }, 300);104 }105106 handleAccountSelect(event) {107 this.selectedAccountId = event.currentTarget.dataset.id;108 // Wire automatically fetches contacts for new accountId109 }110111 // === IMPERATIVE APEX CALL ===112113 async handleCreateAccount() {114 const nameInput = this.template.querySelector('[data-id="name"]');115 const industryInput = this.template.querySelector('[data-id="industry"]');116117 try {118 const newAccount = await createAccount({119 name: nameInput.value,120 industry: industryInput.value121 });122123 this.dispatchEvent(new ShowToastEvent({124 title: 'Success',125 message: 'Account created: ' + newAccount.Name,126 variant: 'success'127 }));128129 // Refresh the wired data to include new account130 await refreshApex(this.wiredAccounts);131132 } catch (error) {133 this.dispatchEvent(new ShowToastEvent({134 title: 'Error',135 message: error.body.message,136 variant: 'error'137 }));138 }139 }140141 // Refresh wired contacts after an update142 async refreshContacts() {143 await refreshApex(this._wiredContactsResult);144 }145}146147// === LIGHTNING DATA SERVICE (No Apex needed) ===148// recordEditor.js149import { LightningElement, api } from 'lwc';150import { updateRecord } from 'lightning/uiRecordApi';151import { ShowToastEvent } from 'lightning/platformShowToastEvent';152import ID_FIELD from '@salesforce/schema/Account.Id';153import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';154155export default class RecordEditor extends LightningElement {156 @api recordId;157158 async handleUpdateIndustry(event) {159 const fields = {};160 fields[ID_FIELD.fieldApiName] = this.recordId;161 fields[INDUSTRY_FIELD.fieldApiName] = event.target.value;162163 try {164 await updateRecord({ fields });165 this.dispatchEvent(new ShowToastEvent({166 title: 'Success',167 message: 'Industry updated!',168 variant: 'success'169 }));170 } catch (error) {171 this.dispatchEvent(new ShowToastEvent({172 title: 'Error',173 message: error.body.message,174 variant: 'error'175 }));176 }177 }178}
🏋️ Practice Exercise
Wire Service Practice:
- Create an Apex controller with @AuraEnabled(cacheable=true) methods and wire them to an LWC
- Build a search component that uses wire with reactive parameters ($ prefix)
- Implement both wire and imperative Apex calls in the same component
- Use refreshApex() to refresh wired data after a DML operation
- Build a CRUD component using only Lightning Data Service (no custom Apex)
- Create a component that uses getPicklistValues to dynamically populate a dropdown
- Implement error handling for both wire and imperative calls with user-friendly messages
- Build a data table using lightning-datatable with wire-connected data
- Create a component that chains multiple Apex calls (fetch accounts, then contacts for selected)
- Write Jest tests that mock wire adapters and Apex calls
⚠️ Common Mistakes
Using @wire with non-cacheable Apex methods — wire requires @AuraEnabled(cacheable=true). Cacheable methods cannot perform DML
Forgetting the $ prefix for reactive wire parameters — @wire(getAccounts, { searchTerm: 'searchTerm' }) is a literal string, not reactive. Use '$searchTerm'
Not holding a reference for refreshApex — you need to store the raw wire result to call refreshApex(this._wiredResult) later
Performing DML in a cacheable method — cacheable=true enforces read-only. Use imperative calls for create/update/delete
Not handling both data and error from wire results — wire always returns { data, error }. Check both to handle loading and error states
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Wire Service & Apex Integration. Login to unlock this feature.