12.4 LWC Data Access: Wire, Imperative & Events
Key Takeaways
- @wire can provision data from Apex (@AuraEnabled(cacheable=true)) and from UI API adapters such as getRecord
- Imperative Apex calls use imported methods as promises for on-demand, non-cacheable, or user-driven operations including DML
- Lightning Data Service / uiRecordApi reduce custom Apex for simple record create, read, update, and delete scenarios
- Child-to-parent communication uses CustomEvent; parent-to-child uses @api properties; unrelated components use LMS or pub-sub patterns
- Always handle wire/imperative error states and avoid silent failures—surface messages with toasts or inline UI
12.4 LWC Data Access: Wire, Imperative & Events
Quick Answer: Use
@wirefor declarative, reactive reads from cacheable Apex or UI API adapters. Use imperative Apex (promise-based) for on-demand calls, DML, and non-cacheable operations. Prefer Lightning Data Service /uiRecordApiwhen standard record access is enough. Communicate parent↔child with@apiandCustomEvent; use Lightning Message Service (LMS) (or a pub-sub pattern) for unrelated components. Always branch ondatavserror.
Data access questions combine UI skills with Apex annotations, caching rules, and composition. This section ties LWC to the server and to other components.
@wire to Apex
Expose read-only Apex for wiring with @AuraEnabled(cacheable=true):
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContacts(Id accountId) {
return [
SELECT Id, Name, Email
FROM Contact
WHERE AccountId = :accountId
ORDER BY Name
LIMIT 50
];
}
}
LWC import and wire:
import { LightningElement, api, wire } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
export default class ContactList extends LightningElement {
@api recordId;
contacts;
error;
@wire(getContacts, { accountId: '$recordId' })
wiredContacts({ data, error }) {
if (data) {
this.contacts = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.contacts = undefined;
}
}
}
Wire rules that appear on exams
| Rule | Detail |
|---|---|
| cacheable=true | Required for Apex methods used with @wire |
| No DML in cacheable methods | Cacheable Apex must be read-only; DML does not belong there |
| Reactive params | '$recordId' re-invokes the wire when the property changes |
| Property vs function form | Property form assigns { data, error }; function form lets you normalize data |
| Sharing & FLS | Server still enforces security—do not treat the client as trusted |
Why wire? Automatic provisioning, reactivity, and platform caching for repeated reads. Ideal for initial page data that depends on recordId or filter fields.
Refreshing wired Apex
When local caches must update after a sibling’s DML (or your own imperative save), use refreshApex on the wired result object you retained from the property form—or re-drive reactive parameters. Exam stems that say “refresh the list after save” often expect imperative save + refreshApex on the wired list.
Imperative Apex Calls
Import the same Apex method and invoke it as a function that returns a Promise:
import { LightningElement, api } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import savePriority from '@salesforce/apex/ContactController.savePriority';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class ContactList extends LightningElement {
@api recordId;
contacts;
async handleLoad() {
try {
this.contacts = await getContacts({ accountId: this.recordId });
} catch (error) {
this.dispatchEvent(
new ShowToastEvent({
title: 'Load failed',
message: this.reduceError(error),
variant: 'error'
})
);
}
}
async handleSave(contactId, priority) {
try {
await savePriority({ contactId, priority });
// then refresh lists / notify parent
} catch (error) {
// handle error
}
}
reduceError(error) {
if (Array.isArray(error?.body)) {
return error.body.map((e) => e.message).join(', ');
}
return error?.body?.message || error?.message || 'Unknown error';
}
}
Apex for DML cannot be cacheable=true:
@AuraEnabled
public static void savePriority(Id contactId, String priority) {
Contact c = new Contact(Id = contactId, Priority__c = priority);
update c;
}
When to choose wire vs imperative
| Need | Prefer |
|---|---|
| Load data when inputs change, read-only | @wire + cacheable Apex or UI API |
| Button click, conditional fetch, dynamic params hard to express | Imperative |
| Insert/update/delete via Apex | Imperative non-cacheable Apex |
| Simple standard record CRUD | LDS / uiRecordApi / record forms first |
Exam trap: Wiring a method that performs DML or lacks cacheable=true is incorrect. Conversely, forcing every read through manual buttons when a reactive wire fits is poor design.
Lightning Data Service & uiRecordApi Overview
Lightning Data Service (LDS) provides client-side caching and shared record state for Lightning UI. In LWC, you typically consume it through lightning/uiRecordApi adapters and through base components that use LDS under the hood.
getRecord / field imports
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import PHONE_FIELD from '@salesforce/schema/Account.Phone';
const FIELDS = [NAME_FIELD, PHONE_FIELD];
export default class AccountHeader extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
account;
get name() {
return getFieldValue(this.account.data, NAME_FIELD);
}
get phone() {
return getFieldValue(this.account.data, PHONE_FIELD);
}
}
Record forms (high productivity)
Base components can remove entire Apex controllers for standard layouts:
<template>
<lightning-record-edit-form object-api-name="Contact" record-id={recordId} onsuccess={handleSuccess}>
<lightning-input-field field-name="FirstName"></lightning-input-field>
<lightning-input-field field-name="LastName"></lightning-input-field>
<lightning-button type="submit" label="Save"></lightning-button>
</lightning-record-edit-form>
</template>
When LDS/UI API is enough: read or edit known standard/custom fields with platform FLS, optimistic UI cache sharing across components on the page, and minimal custom server logic.
When custom Apex is still required: complex SOQL across many objects, custom aggregation, callouts orchestrated with DML, or security/business rules that do not fit form-based CRUD.
Other UI API operations to recognize by name: getRecord, getRecords, updateRecord, createRecord, deleteRecord, getFieldValue, getFieldDisplayValue.
Component Communication: Events & Beyond
Parent → child: @api properties
Parents pass data downward:
<c-priority-badge level={priority}></c-priority-badge>
Child → parent: CustomEvent
// child.js
notifyParent(contactId) {
this.dispatchEvent(
new CustomEvent('contactselect', {
detail: { contactId },
bubbles: false,
composed: false
})
);
}
<!-- parent.html -->
<c-contact-list oncontactselect={handleContactSelect}></c-contact-list>
// parent.js
handleContactSelect(event) {
const { contactId } = event.detail;
this.selectedId = contactId;
}
Conventions:
- Event names in markup are lowercase with an
onprefix (oncontactselect) - Pass payload in
event.detail - Prefer non-bubbling events unless you intentionally design for ancestors further up
Unrelated components: LMS / pub-sub (high level)
When two components do not share a direct parent-child relationship (for example, utility bar vs record detail body):
- Lightning Message Service (LMS) is the modern platform service for publish/subscribe across the DOM tree (and supported containers) using a message channel metadata type
- Older materials mention pubsub utility modules as a lightweight pattern for sibling communication on the same page
Exam heuristic:
| Relationship | Mechanism |
|---|---|
| Parent configures child | @api |
| Child notifies parent | CustomEvent |
| Distant / sibling / cross-dom | LMS (or pub-sub pattern) |
Do not invent application-wide custom DOM hacks to scrape another namespace’s Shadow DOM.
Error Handling Patterns
Robust LWC data code always assumes failure modes: invalid Id, FLS, empty lists, Apex exceptions, and network issues.
Wire errors
@wire(getContacts, { accountId: '$recordId' })
wiredContacts({ data, error }) {
if (data) {
this.contacts = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.contacts = undefined;
}
}
Template:
<template>
<template lwc:if={error}>
<p class="slds-text-color_error">Unable to load contacts.</p>
</template>
<template lwc:if={contacts}>
<!-- render list -->
</template>
</template>
(Exact conditional directives vary by examples you memorize—focus on branching on error vs data, not only happy path.)
Imperative errors
Use try/catch with async/await, or .then/.catch. Normalize Apex error shapes (error.body.message, array of field errors) before showing ShowToastEvent or inline messages.
User feedback
- Toasts for save success/failure
- Spinners while promises are pending
- Inline messages for field-level problems on forms
- Disable submit buttons during in-flight DML to prevent double posts
Security-related “errors”
If users lack field access, UI API and well-written Apex should fail closed or omit fields—not leak data. Prefer with sharing, user-mode / stripInaccessible patterns where appropriate, and least-privilege design.
End-to-End Pattern: List + Save + Notify
- Wire cacheable
getContactsbyrecordId - Child row button imperatively calls non-cacheable Apex update
- On success,
refreshApexthe wired list (or update local state carefully) - Dispatch
CustomEventso a parent highlights the saved record - If a utility component must react, publish on an LMS channel
That storyline stitches every major 12.4 idea into one realistic Lightning page design.
Quick Decision Table
| Scenario | Best tool |
|---|---|
| Show Account Name on record page | getRecord / record view form |
| Query contacts with custom filters | Cacheable Apex + @wire or imperative |
| Update a field on button click | Imperative Apex or updateRecord |
| Admin-editable layout of standard fields | lightning-record-edit-form |
| Child tells parent which row was clicked | CustomEvent |
| Two components on page without hierarchy | LMS / pub-sub |
Common Exam Traps
- Using
@wireon Apex withoutcacheable=true - Putting DML inside cacheable Apex
- Ignoring
erroron wired results - Using application-style global hacks instead of CustomEvent for parent-child
- Writing Apex for a form that
lightning-record-*-formalready handles - Forgetting that imperative Apex returns a Promise (must handle async)
Bottom line: @wire for reactive, cacheable reads (Apex or UI API); imperative Apex for on-demand and DML; LDS/uiRecordApi for standard record work; CustomEvent + @api for hierarchy; LMS/pub-sub for unrelated components; and explicit error handling on every data path.
An LWC uses @wire to call an Apex method that returns a list of Contacts. Which Apex annotation requirement applies?
A child LWC must tell its parent which Contact Id the user selected. What is the standard LWC approach?
When is Lightning Data Service / uiRecordApi generally a better choice than custom Apex in an LWC?