Briefly: if a Lightning Data Service wire adapter can answer your question, use it instead of writing an Apex controller. getRecord, getRelatedListRecords, getObjectInfo and getPicklistValues share one cache across every component on the page, respect field-level security without you writing a line, and update themselves when the record changes. An Apex method does none of that.
The four adapters worth knowing
getRecord reads one record and the fields you name.
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME from '@salesforce/schema/Account.Name';
import OWNER from '@salesforce/schema/Account.Owner.Name';
export default class AccountHeader extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [NAME, OWNER] })
account;
get accountName() {
return getFieldValue(this.account.data, NAME);
}
}
Two details in there matter. The $ makes recordId reactive, so the wire refires when the property changes. Without it the adapter runs once with undefined and never again. And importing fields from @salesforce/schema gives you a compile-time reference, so deleting the field breaks the deployment rather than the page.
getObjectInfo returns metadata about an object: its label, its fields, its record type ids, whether it is queryable. The usual reason to call it is to get the default record type id to feed into a picklist call.
getPicklistValues returns the values for one picklist field under one record type, including the controlling-field mapping for dependent picklists. You almost always chain it behind getObjectInfo:
@wire(getObjectInfo, { objectApiName: CASE_OBJECT })
caseInfo;
@wire(getPicklistValues, {
recordTypeId: '$caseInfo.data.defaultRecordTypeId',
fieldApiName: STATUS_FIELD
})
statusValues;
Hardcoding 012000000000000AAA as the master record type id works and is a habit worth breaking. Chain the adapters instead.
getRelatedListRecords reads a related list without SOQL. You give it the parent record id and the related list id (the relationship API name, such as Contacts or Cases), plus the fields you want:
@wire(getRelatedListRecords, {
parentRecordId: '$recordId',
relatedListId: 'Contacts',
fields: ['Contact.Name', 'Contact.Email'],
pageSize: 10
})
contacts;
Note the field names here are strings, not schema imports, which is an inconsistency in the API rather than something you are doing wrong.
The cache is the actual argument
Every wire adapter reads from and writes to one client-side record cache. Three components on a record page asking for Account.Name produce one server request, not three. When one of them calls updateRecord, the cache changes and all three re-render. Nobody publishes an event, nobody refreshes anything.
That is the behaviour you lose the moment you write @AuraEnabled(cacheable=true). Cacheable Apex has its own cache keyed on the method and its arguments, and it knows nothing about records. Save through LDS and your Apex-backed component still shows the old value. Save through Apex and the LDS components still show the old value. Mixing the two on one page is the single most common cause of stale data in a Lightning page.
When Apex is genuinely the answer
LDS adapters read one record, or one related list, or metadata. They do not aggregate, they do not join across unrelated objects, and they do not run business logic. Write Apex when you need:
- A query with a filter, a sort, or an aggregate that the adapters cannot express
- Data from several objects assembled into one shape
- A callout, or anything with side effects
- Records the running user should see through sharing rules you are deliberately bypassing
Do not write Apex because the wire returned the data in an awkward shape. getFieldValue and a getter fix that.
The three-state problem
A wire result has data, error, and a third state where both are undefined because the call has not come back. Template code that assumes the first state is how you get Cannot read properties of undefined in the console on every page load. Guard with a getter or with template lwc:if, and use optional chaining in JavaScript:
get ownerName() {
return this.account?.data?.fields?.Owner?.value?.fields?.Name?.value;
}
That chain is ugly. getFieldValue exists precisely so you do not write it, and it handles the spanning field for you.
Frequently asked
How do I force a wire to refetch? Import refreshApex and call it on the wired property, or for record data call notifyRecordUpdateAvailable with the record ids so LDS revalidates its cache.
Can I wire a function instead of a property? Yes, and you should when you need to react to the result. The function receives { data, error } and runs on every emission, including the first empty one.
Do wire adapters respect sharing? They run in the user context, so sharing and field-level security both apply. There is no way to make them ignore either, which is usually the point.
Why is my wire firing with undefined parameters? A reactive parameter that starts undefined fires the adapter anyway. Either guard inside a wired function or accept the extra emission.
