Wire Service & Apex Integration

0/4 in this phase0/41 across the roadmap

📖 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 @wire decorator 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:

  1. 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
  2. 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 record
  • getRecords — Read multiple records
  • createRecord — Create a record
  • updateRecord — Update a record
  • deleteRecord — Delete a record
  • getObjectInfo — Get object metadata
  • getPicklistValues — Get picklist options

💻 Code Example

codeTap to expand ⛶
1// Wire Service & Apex Integration
2
3// === APEX CONTROLLER ===
4// AccountController.cls
5public with sharing class AccountController {
6
7 // 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, CreatedDate
13 FROM Account
14 WHERE Name LIKE :searchKey
15 ORDER BY Name
16 LIMIT 20
17 ];
18 }
19
20 @AuraEnabled(cacheable=true)
21 public static List<Contact> getContactsByAccount(Id accountId) {
22 return [
23 SELECT Id, FirstName, LastName, Email, Phone, Title
24 FROM Contact
25 WHERE AccountId = :accountId
26 ORDER BY LastName
27 ];
28 }
29
30 // Non-cacheable — for imperative calls (DML operations)
31 @AuraEnabled
32 public static Account createAccount(String name, String industry) {
33 Account acc = new Account(Name = name, Industry = industry);
34 insert acc;
35 return acc;
36 }
37
38 @AuraEnabled
39 public static void updateAccountIndustry(Id accountId, String industry) {
40 Account acc = new Account(Id = accountId, Industry = industry);
41 update acc;
42 }
43}
44
45// === LWC — WIRE SERVICE ===
46// accountSearch.js
47import { 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';
53
54export default class AccountSearch extends LightningElement {
55 searchTerm = '';
56 selectedAccountId;
57
58 // === WIRE — Reactive data fetching ===
59
60 // Wire to Apex method (re-queries when searchTerm changes)
61 @wire(getAccounts, { searchTerm: '$searchTerm' })
62 wiredAccounts; // { data: [...], error: {...} }
63
64 // Wire with hold reference for refresh
65 _wiredContactsResult;
66 contacts;
67 contactsError;
68
69 @wire(getContactsByAccount, { accountId: '$selectedAccountId' })
70 wiredContacts(result) {
71 this._wiredContactsResult = result; // Hold ref for refreshApex
72 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 }
81
82 // Getters
83 get accounts() {
84 return this.wiredAccounts.data || [];
85 }
86
87 get hasAccounts() {
88 return this.accounts.length > 0;
89 }
90
91 get isLoading() {
92 return !this.wiredAccounts.data && !this.wiredAccounts.error;
93 }
94
95 // === EVENT HANDLERS ===
96
97 handleSearchChange(event) {
98 // Debounce search input
99 clearTimeout(this._debounceTimer);
100 this._debounceTimer = setTimeout(() => {
101 this.searchTerm = event.target.value;
102 // Wire automatically re-fetches when searchTerm changes
103 }, 300);
104 }
105
106 handleAccountSelect(event) {
107 this.selectedAccountId = event.currentTarget.dataset.id;
108 // Wire automatically fetches contacts for new accountId
109 }
110
111 // === IMPERATIVE APEX CALL ===
112
113 async handleCreateAccount() {
114 const nameInput = this.template.querySelector('[data-id="name"]');
115 const industryInput = this.template.querySelector('[data-id="industry"]');
116
117 try {
118 const newAccount = await createAccount({
119 name: nameInput.value,
120 industry: industryInput.value
121 });
122
123 this.dispatchEvent(new ShowToastEvent({
124 title: 'Success',
125 message: 'Account created: ' + newAccount.Name,
126 variant: 'success'
127 }));
128
129 // Refresh the wired data to include new account
130 await refreshApex(this.wiredAccounts);
131
132 } catch (error) {
133 this.dispatchEvent(new ShowToastEvent({
134 title: 'Error',
135 message: error.body.message,
136 variant: 'error'
137 }));
138 }
139 }
140
141 // Refresh wired contacts after an update
142 async refreshContacts() {
143 await refreshApex(this._wiredContactsResult);
144 }
145}
146
147// === LIGHTNING DATA SERVICE (No Apex needed) ===
148// recordEditor.js
149import { 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';
154
155export default class RecordEditor extends LightningElement {
156 @api recordId;
157
158 async handleUpdateIndustry(event) {
159 const fields = {};
160 fields[ID_FIELD.fieldApiName] = this.recordId;
161 fields[INDUSTRY_FIELD.fieldApiName] = event.target.value;
162
163 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:

  1. Create an Apex controller with @AuraEnabled(cacheable=true) methods and wire them to an LWC
  2. Build a search component that uses wire with reactive parameters ($ prefix)
  3. Implement both wire and imperative Apex calls in the same component
  4. Use refreshApex() to refresh wired data after a DML operation
  5. Build a CRUD component using only Lightning Data Service (no custom Apex)
  6. Create a component that uses getPicklistValues to dynamically populate a dropdown
  7. Implement error handling for both wire and imperative calls with user-friendly messages
  8. Build a data table using lightning-datatable with wire-connected data
  9. Create a component that chains multiple Apex calls (fetch accounts, then contacts for selected)
  10. 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.