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
Last updated: August 2026

12.4 LWC Data Access: Wire, Imperative & Events

Quick Answer: Use @wire for 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 / uiRecordApi when standard record access is enough. Communicate parent↔child with @api and CustomEvent; use Lightning Message Service (LMS) (or a pub-sub pattern) for unrelated components. Always branch on data vs error.

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

RuleDetail
cacheable=trueRequired for Apex methods used with @wire
No DML in cacheable methodsCacheable Apex must be read-only; DML does not belong there
Reactive params'$recordId' re-invokes the wire when the property changes
Property vs function formProperty form assigns { data, error }; function form lets you normalize data
Sharing & FLSServer 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

NeedPrefer
Load data when inputs change, read-only@wire + cacheable Apex or UI API
Button click, conditional fetch, dynamic params hard to expressImperative
Insert/update/delete via ApexImperative non-cacheable Apex
Simple standard record CRUDLDS / 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 on prefix (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:

RelationshipMechanism
Parent configures child@api
Child notifies parentCustomEvent
Distant / sibling / cross-domLMS (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

  1. Wire cacheable getContacts by recordId
  2. Child row button imperatively calls non-cacheable Apex update
  3. On success, refreshApex the wired list (or update local state carefully)
  4. Dispatch CustomEvent so a parent highlights the saved record
  5. 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

ScenarioBest tool
Show Account Name on record pagegetRecord / record view form
Query contacts with custom filtersCacheable Apex + @wire or imperative
Update a field on button clickImperative Apex or updateRecord
Admin-editable layout of standard fieldslightning-record-edit-form
Child tells parent which row was clickedCustomEvent
Two components on page without hierarchyLMS / pub-sub

Common Exam Traps

  • Using @wire on Apex without cacheable=true
  • Putting DML inside cacheable Apex
  • Ignoring error on wired results
  • Using application-style global hacks instead of CustomEvent for parent-child
  • Writing Apex for a form that lightning-record-*-form already 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.

Test Your Knowledge

An LWC uses @wire to call an Apex method that returns a list of Contacts. Which Apex annotation requirement applies?

A
B
C
D
Test Your Knowledge

A child LWC must tell its parent which Contact Id the user selected. What is the standard LWC approach?

A
B
C
D
Test Your Knowledge

When is Lightning Data Service / uiRecordApi generally a better choice than custom Apex in an LWC?

A
B
C
D