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?
The method must be @AuraEnabled(cacheable=true) and must not perform DML
The method must be cacheable=false and must perform at least one DML statement
The method cannot be static and must be an instance method on a non-sharing class only
@wire works only with Visualforce controllers, not Apex classes
A child LWC must tell its parent which Contact Id the user selected. What is the standard LWC approach?
Have the child update a static global variable on window that the parent polls
Dispatch a CustomEvent with the Contact Id in event.detail and handle it in the parent template
Require an Aura application event even though both components are LWC
Query the parent’s Shadow DOM from the child and set fields directly
When is Lightning Data Service / uiRecordApi generally a better choice than custom Apex in an LWC?
When the UI only needs standard create/read/update/delete of records and fields the user can access
When the UI must perform a multi-object aggregation callout orchestration that no UI API supports
When the developer wants to bypass field-level security for all users
When cacheable=true DML is required inside getRecord
Sections you finish are checked off in the contents.