Part 83: The Lightning Data Service in LWC
Welcome back to the Salesforce series. In previous parts we covered the @wire decorator (Part 78) and base Lightning components (Part 81). Those gave us the building blocks for reactive data fetching and pre-built UI elements. Now we need to put those concepts together and learn the primary way Lightning Web Components interact with Salesforce data without writing a single line of Apex.
The Lightning Data Service, or LDS, is a client-side framework that sits between your components and the Salesforce server. It manages data access, caching, and synchronization so that every component on the page that references the same record sees the same data. When one component updates a record through LDS, every other component watching that record gets the updated values automatically. No page refresh, no manual event wiring, no Apex controller needed.
This is a large section because LDS covers a lot of ground. We will start with the basics of retrieving records, move through the three record form components, learn how to intercept form submissions, and then go deep into the uiAPI modules that give you full programmatic control over record operations, navigation menus, list views, object metadata, and related lists.
Let us get started.
What is the Lightning Data Service?
The Lightning Data Service is Salesforce’s built-in data layer for Lightning. It provides a shared cache that all components on the page can read from and write to. When you use LDS, you do not need to write Apex to perform basic CRUD operations on records. LDS handles the server communication, caching, conflict resolution, and security enforcement (FLS and CRUD permissions) for you.
There are three main ways to use LDS in LWC:
- Record form components — pre-built UI components that render forms automatically based on record metadata.
- Wire adapters — functions you use with the
@wiredecorator to read data reactively. - Imperative functions — JavaScript functions you call directly to create, update, or delete records.
The record form components are the fastest way to get a working form on screen. The wire adapters and imperative functions give you more control when you need custom layouts, conditional logic, or chained operations.
LDS respects all Salesforce security settings. If a user does not have access to a field, LDS will not return it. If a user does not have permission to update a record, the update will fail with an appropriate error. You do not need to implement these checks yourself.
How to Retrieve Salesforce Records Using LDS
The most common way to retrieve a single record is the getRecord wire adapter from lightning/uiRecordApi. You provide the record ID and specify which fields you want, and LDS returns the data reactively.
import { LightningElement, wire, api } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import PHONE_FIELD from '@salesforce/schema/Account.Phone';
export default class AccountDetail extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, INDUSTRY_FIELD, PHONE_FIELD] })
account;
get accountName() {
return getFieldValue(this.account.data, NAME_FIELD);
}
get accountIndustry() {
return getFieldValue(this.account.data, INDUSTRY_FIELD);
}
get accountPhone() {
return getFieldValue(this.account.data, PHONE_FIELD);
}
}
<template>
<lightning-card title="Account Detail">
<template if:true={account.data}>
<p>Name: {accountName}</p>
<p>Industry: {accountIndustry}</p>
<p>Phone: {accountPhone}</p>
</template>
<template if:true={account.error}>
<p>Error loading account.</p>
</template>
</lightning-card>
</template>
The $recordId syntax tells the wire adapter to re-run whenever the recordId property changes. The getFieldValue helper extracts the value from the nested wire result structure. You can also use getFieldDisplayValue if you want the formatted display value instead of the raw value.
You can also pass optionalFields instead of fields. The difference is that fields will throw an error if the user does not have access to one of the specified fields, while optionalFields will silently omit any inaccessible fields from the result.
@wire(getRecord, {
recordId: '$recordId',
fields: [NAME_FIELD],
optionalFields: [INDUSTRY_FIELD, PHONE_FIELD]
})
account;
The Lightning Record View Form
The lightning-record-view-form component renders a read-only view of a record. You give it a record ID and object API name, then place lightning-output-field components inside it to specify which fields to display. It handles all the data fetching and rendering automatically.
<template>
<lightning-record-view-form
record-id={recordId}
object-api-name="Contact"
>
<div class="slds-grid slds-wrap">
<div class="slds-col slds-size_1-of-2">
<lightning-output-field field-name="FirstName"></lightning-output-field>
<lightning-output-field field-name="LastName"></lightning-output-field>
<lightning-output-field field-name="Email"></lightning-output-field>
</div>
<div class="slds-col slds-size_1-of-2">
<lightning-output-field field-name="Phone"></lightning-output-field>
<lightning-output-field field-name="Title"></lightning-output-field>
<lightning-output-field field-name="AccountId"></lightning-output-field>
</div>
</div>
</lightning-record-view-form>
</template>
import { LightningElement, api } from 'lwc';
export default class ContactViewForm extends LightningElement {
@api recordId;
}
This component is useful when you want a quick, styled read-only display of record data without building the layout from scratch. The output fields automatically render with the correct format for their data type — dates, currencies, lookups, and picklists all display correctly.
You can also use the density attribute to control the spacing. Setting density="comfy" gives more padding, while density="compact" tightens the layout.
The Lightning Record Edit Form
The lightning-record-edit-form component provides an editable form with built-in save functionality. It uses lightning-input-field components instead of output fields, and it automatically handles validation, error display, and the save operation.
<template>
<lightning-record-edit-form
record-id={recordId}
object-api-name="Contact"
onsuccess={handleSuccess}
onerror={handleError}
>
<lightning-messages></lightning-messages>
<lightning-input-field field-name="FirstName"></lightning-input-field>
<lightning-input-field field-name="LastName"></lightning-input-field>
<lightning-input-field field-name="Email"></lightning-input-field>
<lightning-input-field field-name="Phone"></lightning-input-field>
<lightning-input-field field-name="Title"></lightning-input-field>
<div class="slds-m-top_medium">
<lightning-button variant="brand" type="submit" label="Save"></lightning-button>
</div>
</lightning-record-edit-form>
</template>
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class ContactEditForm extends LightningElement {
@api recordId;
handleSuccess(event) {
const toastEvent = new ShowToastEvent({
title: 'Success',
message: 'Contact updated successfully.',
variant: 'success'
});
this.dispatchEvent(toastEvent);
}
handleError(event) {
const toastEvent = new ShowToastEvent({
title: 'Error',
message: event.detail.message,
variant: 'error'
});
this.dispatchEvent(toastEvent);
}
}
A few important things to note. The lightning-messages component displays server-side validation errors automatically. The type="submit" on the button triggers the form submission. If you omit the record-id attribute, the form operates in create mode instead of edit mode.
To create a new record using the edit form, simply leave out the record ID:
<template>
<lightning-record-edit-form
object-api-name="Contact"
onsuccess={handleSuccess}
>
<lightning-messages></lightning-messages>
<lightning-input-field field-name="FirstName"></lightning-input-field>
<lightning-input-field field-name="LastName"></lightning-input-field>
<lightning-input-field field-name="Email"></lightning-input-field>
<div class="slds-m-top_medium">
<lightning-button variant="brand" type="submit" label="Create Contact"></lightning-button>
</div>
</lightning-record-edit-form>
</template>
You can also pre-populate field values by setting the value attribute on individual input fields, or by passing a record-type-id to the form to control which record type page layout is used.
The Lightning Record Form Component
The lightning-record-form component is the simplest of the three. It combines view and edit modes into a single component. You pass it a list of fields, and it renders either a view form or an edit form depending on its mode attribute.
<template>
<lightning-record-form
record-id={recordId}
object-api-name="Opportunity"
fields={fields}
mode="view"
columns="2"
onsuccess={handleSuccess}
></lightning-record-form>
</template>
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import NAME_FIELD from '@salesforce/schema/Opportunity.Name';
import STAGE_FIELD from '@salesforce/schema/Opportunity.StageName';
import AMOUNT_FIELD from '@salesforce/schema/Opportunity.Amount';
import CLOSE_DATE_FIELD from '@salesforce/schema/Opportunity.CloseDate';
export default class OpportunityForm extends LightningElement {
@api recordId;
fields = [NAME_FIELD, STAGE_FIELD, AMOUNT_FIELD, CLOSE_DATE_FIELD];
handleSuccess() {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Opportunity saved.',
variant: 'success'
})
);
}
}
The mode attribute accepts three values:
view— renders a read-only form with an edit button. Clicking the button switches to edit mode inline.edit— renders an editable form with save and cancel buttons.readonly— renders a read-only form with no edit button.
The columns attribute controls how many columns the form uses. The layout-type attribute can be set to Full or Compact to use the object’s page layout or compact layout respectively. When you use layout-type, you do not need to specify individual fields — the form automatically includes the fields from the layout.
<lightning-record-form
record-id={recordId}
object-api-name="Account"
layout-type="Full"
mode="view"
columns="2"
></lightning-record-form>
How to Intercept and Customize the Lightning Record Forms Server Submission
Sometimes you need to modify the data before it goes to the server, or you need to run custom validation logic. The lightning-record-edit-form fires an onsubmit event before sending data to the server. You can intercept this event, modify the fields, and then either continue with the submission or cancel it entirely.
<template>
<lightning-record-edit-form
record-id={recordId}
object-api-name="Case"
onsubmit={handleSubmit}
onsuccess={handleSuccess}
>
<lightning-messages></lightning-messages>
<lightning-input-field field-name="Subject"></lightning-input-field>
<lightning-input-field field-name="Description"></lightning-input-field>
<lightning-input-field field-name="Priority"></lightning-input-field>
<lightning-input-field field-name="Status"></lightning-input-field>
<div class="slds-m-top_medium">
<lightning-button variant="brand" type="submit" label="Save Case"></lightning-button>
</div>
</lightning-record-edit-form>
</template>
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class CaseEditForm extends LightningElement {
@api recordId;
handleSubmit(event) {
event.preventDefault();
const fields = event.detail.fields;
// Custom validation
if (!fields.Subject || fields.Subject.trim() === '') {
this.dispatchEvent(
new ShowToastEvent({
title: 'Validation Error',
message: 'Subject is required.',
variant: 'error'
})
);
return;
}
// Modify field values before submission
fields.Status = 'New';
fields.Description = fields.Description
? fields.Description + '\n\n[Submitted via custom form]'
: '[Submitted via custom form]';
// Resume the form submission with modified fields
this.template.querySelector('lightning-record-edit-form').submit(fields);
}
handleSuccess(event) {
const recordId = event.detail.id;
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Case saved with ID: ' + recordId,
variant: 'success'
})
);
}
}
The key steps here are: call event.preventDefault() to stop the automatic submission, read the field values from event.detail.fields, modify them as needed, run any validation logic, and then call .submit(fields) on the form element to resume the submission with your modified data. If your validation fails, simply return without calling .submit() and the form will not save.
You can also use the onload event to run logic when the form finishes loading its data, which is useful for setting default values or showing/hiding fields based on the loaded record.
An Introduction to the uiAPI in LWC
The record form components are powerful, but they control the UI. When you need full control over the user interface and just want to interact with data programmatically, you use the Lightning UI API modules. These are a set of wire adapters and imperative functions that let you perform CRUD operations, read object metadata, access picklist values, query list views, and more — all without writing Apex.
The uiAPI is split across several modules:
lightning/uiRecordApi— create, read, update, and delete records.lightning/uiAppsApi— access navigation menu items.lightning/uiListApi— work with list views.lightning/uiObjectInfoApi— get object and picklist metadata.lightning/uiRelatedListApi— read related list data and metadata.
All of these modules work through LDS, which means they share the same cache. A record fetched through getRecord and a record displayed in a lightning-record-view-form on the same page will always be in sync.
Create Records Using the uiAPI (uiRecordAPI)
To create a record programmatically, import createRecord from lightning/uiRecordApi. This is an imperative function that returns a promise.
import { LightningElement, track } from 'lwc';
import { createRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import PHONE_FIELD from '@salesforce/schema/Account.Phone';
export default class CreateAccount extends LightningElement {
accountName = '';
industry = '';
phone = '';
handleNameChange(event) {
this.accountName = event.target.value;
}
handleIndustryChange(event) {
this.industry = event.target.value;
}
handlePhoneChange(event) {
this.phone = event.target.value;
}
createAccount() {
const fields = {};
fields[NAME_FIELD.fieldApiName] = this.accountName;
fields[INDUSTRY_FIELD.fieldApiName] = this.industry;
fields[PHONE_FIELD.fieldApiName] = this.phone;
const recordInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };
createRecord(recordInput)
.then(result => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Account created with ID: ' + result.id,
variant: 'success'
})
);
this.accountName = '';
this.industry = '';
this.phone = '';
})
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: error.body.message,
variant: 'error'
})
);
});
}
}
<template>
<lightning-card title="Create Account">
<div class="slds-p-around_medium">
<lightning-input label="Account Name" value={accountName} onchange={handleNameChange}></lightning-input>
<lightning-input label="Industry" value={industry} onchange={handleIndustryChange}></lightning-input>
<lightning-input label="Phone" value={phone} onchange={handlePhoneChange}></lightning-input>
<div class="slds-m-top_medium">
<lightning-button variant="brand" label="Create" onclick={createAccount}></lightning-button>
</div>
</div>
</lightning-card>
</template>
The createRecord function takes a record input object with apiName (the object type) and fields (a map of field API names to values). It returns a promise that resolves with the created record, including its new ID.
Update Records Using the uiAPI (uiRecordAPI)
Updating records follows a similar pattern. Import updateRecord and pass it a record input object that includes the record ID and the fields to update.
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import { updateRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import ID_FIELD from '@salesforce/schema/Contact.Id';
import PHONE_FIELD from '@salesforce/schema/Contact.Phone';
import TITLE_FIELD from '@salesforce/schema/Contact.Title';
export default class UpdateContact extends LightningElement {
@api recordId;
phone = '';
title = '';
@wire(getRecord, { recordId: '$recordId', fields: [PHONE_FIELD, TITLE_FIELD] })
wiredContact({ data, error }) {
if (data) {
this.phone = getFieldValue(data, PHONE_FIELD);
this.title = getFieldValue(data, TITLE_FIELD);
}
}
handlePhoneChange(event) {
this.phone = event.target.value;
}
handleTitleChange(event) {
this.title = event.target.value;
}
updateContact() {
const fields = {};
fields[ID_FIELD.fieldApiName] = this.recordId;
fields[PHONE_FIELD.fieldApiName] = this.phone;
fields[TITLE_FIELD.fieldApiName] = this.title;
const recordInput = { fields };
updateRecord(recordInput)
.then(() => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Contact updated.',
variant: 'success'
})
);
})
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: error.body.message,
variant: 'error'
})
);
});
}
}
<template>
<lightning-card title="Update Contact">
<div class="slds-p-around_medium">
<lightning-input label="Phone" value={phone} onchange={handlePhoneChange}></lightning-input>
<lightning-input label="Title" value={title} onchange={handleTitleChange}></lightning-input>
<div class="slds-m-top_medium">
<lightning-button variant="brand" label="Update" onclick={updateContact}></lightning-button>
</div>
</div>
</lightning-card>
</template>
The critical difference between create and update is that for updates, you must include the record’s Id field in the fields map. You do not need to pass the apiName for updates — LDS infers the object type from the record ID. After a successful update, LDS automatically refreshes any components on the page that reference the same record.
Delete Records Using the uiAPI (uiRecordAPI)
Deleting a record is the simplest operation. Import deleteRecord and pass it the record ID.
import { LightningElement, api } from 'lwc';
import { deleteRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { NavigationMixin } from 'lightning/navigation';
export default class DeleteRecord extends NavigationMixin(LightningElement) {
@api recordId;
handleDelete() {
deleteRecord(this.recordId)
.then(() => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Record deleted.',
variant: 'success'
})
);
this[NavigationMixin.Navigate]({
type: 'standard__objectPage',
attributes: {
objectApiName: 'Account',
actionName: 'home'
}
});
})
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: error.body.message,
variant: 'error'
})
);
});
}
}
<template>
<lightning-card title="Delete Record">
<div class="slds-p-around_medium">
<p>Are you sure you want to delete this record?</p>
<div class="slds-m-top_medium">
<lightning-button variant="destructive" label="Delete" onclick={handleDelete}></lightning-button>
</div>
</div>
</lightning-card>
</template>
After deletion, the example uses NavigationMixin to redirect the user to the object’s home page, since the record no longer exists. The deleteRecord function takes a single argument — the record ID — and returns a promise.
Get Nav Menu Items Using the uiAPI (uiAppsAPI)
The lightning/uiAppsApi module provides the getNavItems wire adapter, which retrieves the navigation menu items for a given app.
import { LightningElement, wire } from 'lwc';
import { getNavItems } from 'lightning/uiAppsApi';
export default class NavMenuItems extends LightningElement {
@wire(getNavItems, {
formFactor: 'Large',
type: 'Standard',
page: 0,
pageSize: 25
})
navItems;
get items() {
if (this.navItems.data) {
return this.navItems.data.navItems.map(item => ({
label: item.label,
name: item.name,
objectApiName: item.objectApiName
}));
}
return [];
}
}
<template>
<lightning-card title="Navigation Menu Items">
<template if:true={items.length}>
<ul class="slds-list_dotted slds-p-around_medium">
<template for:each={items} for:item="item">
<li key={item.name}>{item.label} ({item.objectApiName})</li>
</template>
</ul>
</template>
</lightning-card>
</template>
This is useful when you need to build custom navigation components or display the available tabs for the current app. The formFactor parameter controls whether you get items for desktop (Large), tablet (Medium), or phone (Small).
Get List View Info Using the uiAPI (uiListAPI)
The lightning/uiListsApi module gives you access to list view metadata and data. The getListUi wire adapter fetches the records and columns for a specific list view.
import { LightningElement, wire } from 'lwc';
import { getListUi } from 'lightning/uiListsApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
export default class AccountListView extends LightningElement {
@wire(getListUi, {
objectApiName: ACCOUNT_OBJECT,
listViewApiName: 'AllAccounts',
pageSize: 10
})
listView;
get records() {
if (this.listView.data) {
return this.listView.data.records.records;
}
return [];
}
get columns() {
if (this.listView.data) {
return this.listView.data.info.displayColumns.map(col => ({
label: col.label,
fieldApiName: col.fieldApiName,
sortable: col.sortable
}));
}
return [];
}
}
<template>
<lightning-card title="Account List View">
<template if:true={columns.length}>
<table class="slds-table slds-table_bordered slds-m-around_medium">
<thead>
<tr>
<template for:each={columns} for:item="col">
<th key={col.fieldApiName}>{col.label}</th>
</template>
</tr>
</thead>
<tbody>
<template for:each={records} for:item="record">
<tr key={record.id}>
<template for:each={columns} for:item="col">
<td key={col.fieldApiName}>
{record.fields[col.fieldApiName].value}
</td>
</template>
</tr>
</template>
</tbody>
</table>
</template>
</lightning-card>
</template>
The listViewApiName can be AllAccounts, RecentlyViewedAccounts, MyAccounts, or any custom list view API name. You can also use listViewId instead if you have the 18-character ID of the list view. The result includes both the records and the display column metadata, which makes it straightforward to build custom data tables.
Get Object Information Using the uiAPI (uiObjectAPI)
The lightning/uiObjectInfoApi module provides the getObjectInfo wire adapter, which returns metadata about a Salesforce object — its fields, record types, label, API name, and more.
import { LightningElement, wire } from 'lwc';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import CASE_OBJECT from '@salesforce/schema/Case';
export default class ObjectInfoExample extends LightningElement {
@wire(getObjectInfo, { objectApiName: CASE_OBJECT })
objectInfo;
get fieldNames() {
if (this.objectInfo.data) {
return Object.keys(this.objectInfo.data.fields).map(fieldName => {
const field = this.objectInfo.data.fields[fieldName];
return {
apiName: field.apiName,
label: field.label,
dataType: field.dataType,
required: field.required
};
});
}
return [];
}
get recordTypeInfos() {
if (this.objectInfo.data) {
return Object.values(this.objectInfo.data.recordTypeInfos).map(rt => ({
name: rt.name,
recordTypeId: rt.recordTypeId,
isDefault: rt.defaultRecordTypeMapping
}));
}
return [];
}
}
<template>
<lightning-card title="Case Object Info">
<template if:true={objectInfo.data}>
<p class="slds-p-around_medium">
<strong>Label:</strong> {objectInfo.data.label} |
<strong>API Name:</strong> {objectInfo.data.apiName} |
<strong>Key Prefix:</strong> {objectInfo.data.keyPrefix}
</p>
<h3 class="slds-text-heading_small slds-p-left_medium">Fields ({fieldNames.length})</h3>
<ul class="slds-list_dotted slds-p-around_medium">
<template for:each={fieldNames} for:item="field">
<li key={field.apiName}>
{field.label} ({field.apiName}) - {field.dataType}
<template if:true={field.required}> [Required]</template>
</li>
</template>
</ul>
</template>
</lightning-card>
</template>
This adapter is extremely useful for building dynamic components. You can inspect field types at runtime, build forms dynamically based on object metadata, filter fields by data type, or check which record types are available before rendering a form.
Get Picklist Information Using the uiAPI (uiObjectAPI)
The getPicklistValues and getPicklistValuesByRecordType wire adapters from lightning/uiObjectInfoApi give you access to picklist field options.
import { LightningElement, wire } from 'lwc';
import { getObjectInfo, getPicklistValues } from 'lightning/uiObjectInfoApi';
import CASE_OBJECT from '@salesforce/schema/Case';
import STATUS_FIELD from '@salesforce/schema/Case.Status';
export default class PicklistExample extends LightningElement {
@wire(getObjectInfo, { objectApiName: CASE_OBJECT })
objectInfo;
@wire(getPicklistValues, {
recordTypeId: '$defaultRecordTypeId',
fieldApiName: STATUS_FIELD
})
statusPicklist;
get defaultRecordTypeId() {
if (this.objectInfo.data) {
return this.objectInfo.data.defaultRecordTypeId;
}
return undefined;
}
get statusOptions() {
if (this.statusPicklist.data) {
return this.statusPicklist.data.values.map(option => ({
label: option.label,
value: option.value
}));
}
return [];
}
selectedStatus = '';
handleStatusChange(event) {
this.selectedStatus = event.detail.value;
}
}
<template>
<lightning-card title="Case Status Picklist">
<div class="slds-p-around_medium">
<template if:true={statusOptions.length}>
<lightning-combobox
label="Status"
value={selectedStatus}
options={statusOptions}
onchange={handleStatusChange}
></lightning-combobox>
</template>
</div>
</lightning-card>
</template>
The getPicklistValues adapter requires a recordTypeId. This is why the example first wires getObjectInfo to get the default record type ID, then passes it reactively to getPicklistValues using the $defaultRecordTypeId syntax. The adapter respects record type-specific picklist value restrictions, so you always get the correct set of options.
For getting all picklist values for an entire record type at once, use getPicklistValuesByRecordType:
import { getPicklistValuesByRecordType } from 'lightning/uiObjectInfoApi';
@wire(getPicklistValuesByRecordType, {
objectApiName: CASE_OBJECT,
recordTypeId: '$defaultRecordTypeId'
})
allPicklists;
This returns all picklist fields and their values for the specified record type in a single call, which is more efficient than making individual getPicklistValues calls for each field.
Get Related List Information Using the uiAPI (uiRelatedListAPI)
The lightning/uiRelatedListApi module provides wire adapters for working with related lists. You can get the records in a related list, the metadata about a related list, or a summary count of related records.
import { LightningElement, api, wire } from 'lwc';
import { getRelatedListRecords } from 'lightning/uiRelatedListApi';
export default class RelatedContacts extends LightningElement {
@api recordId;
@wire(getRelatedListRecords, {
parentRecordId: '$recordId',
relatedListId: 'Contacts',
fields: ['Contact.Name', 'Contact.Email', 'Contact.Phone'],
pageSize: 10
})
relatedContacts;
get contacts() {
if (this.relatedContacts.data) {
return this.relatedContacts.data.records.map(record => ({
id: record.id,
name: record.fields.Name.value,
email: record.fields.Email.value,
phone: record.fields.Phone.value
}));
}
return [];
}
get hasContacts() {
return this.contacts.length > 0;
}
}
<template>
<lightning-card title="Related Contacts">
<template if:true={hasContacts}>
<table class="slds-table slds-table_bordered slds-m-around_medium">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<template for:each={contacts} for:item="contact">
<tr key={contact.id}>
<td>{contact.name}</td>
<td>{contact.email}</td>
<td>{contact.phone}</td>
</tr>
</template>
</tbody>
</table>
</template>
<template if:false={hasContacts}>
<p class="slds-p-around_medium">No related contacts found.</p>
</template>
</lightning-card>
</template>
You can also use getRelatedListInfo to get metadata about a related list (its columns, label, and sort information) and getRelatedListCount to get just the count of related records without fetching the actual data:
import { getRelatedListInfo } from 'lightning/uiRelatedListApi';
import { getRelatedListCount } from 'lightning/uiRelatedListApi';
@wire(getRelatedListInfo, {
parentObjectApiName: 'Account',
relatedListId: 'Contacts'
})
contactListInfo;
@wire(getRelatedListCount, {
parentRecordId: '$recordId',
relatedListId: 'Contacts'
})
contactCount;
The relatedListId corresponds to the relationship name. For standard relationships, this is usually the plural object name (like Contacts on Account). For custom relationships, it is the relationship name defined on the lookup or master-detail field (like Custom_Objects__r).
Section Notes
The Lightning Data Service is the preferred way to interact with Salesforce data in LWC. It eliminates the need for Apex in most standard CRUD scenarios, provides automatic caching and cross-component synchronization, and enforces security at the platform level.
Start with the record form components when you need a quick UI. Use lightning-record-form for the simplest cases, lightning-record-view-form when you need read-only display with custom layout, and lightning-record-edit-form when you need editable forms with custom validation. When you need full programmatic control, switch to the uiAPI functions — createRecord, updateRecord, and deleteRecord for data operations, and the various wire adapters for metadata, picklists, list views, and related lists.
One important limitation: LDS is designed for single-record operations. If you need to query multiple records with complex filters, aggregate data, or perform bulk operations, you will still need Apex. But for the majority of component-level record interactions, LDS keeps your code simpler, faster, and more maintainable.
In the next section, we will continue exploring more advanced LWC patterns and platform integrations. See you there.