fe-lwc-skill 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ # data-lds-first
2
+
3
+ **Impact: CRITICAL**
4
+
5
+ Use `lightning/uiRecordApi` for standard CRUD and record fetching before writing any Apex controller. LDS is backed by the **UI API cache** — the platform automatically deduplicates requests and shares cached data across all components on the page.
6
+
7
+ > **Rule:** If the operation is get / create / update / delete on a standard or custom object, reach for LDS first. Only fall back to Apex when LDS cannot cover the requirement (e.g. complex queries, aggregates, multi-object logic).
8
+
9
+ ---
10
+
11
+ ## Get a single record
12
+
13
+ **Incorrect — unnecessary Apex call, no caching:**
14
+
15
+ ```typescript
16
+ // patientCard.ts
17
+ import { LightningElement, api, wire } from "lwc";
18
+ import getPatient from "@salesforce/apex/PatientController.getPatient";
19
+
20
+ // @ts-ignore
21
+ export default class PatientCard extends LightningElement {
22
+ // @ts-ignore
23
+ @api recordId: string;
24
+
25
+ // @ts-ignore
26
+ @wire(getPatient, { recordId: "$recordId" })
27
+ patient: { data?: unknown; error?: unknown };
28
+ }
29
+ ```
30
+
31
+ **Correct — LDS handles caching, no SOQL consumed:**
32
+
33
+ ```typescript
34
+ // patientCard.ts
35
+ import { LightningElement, api, wire } from "lwc";
36
+ import { getRecord, getFieldValue } from "lightning/uiRecordApi";
37
+ import NAME_FIELD from "@salesforce/schema/Patient__c.Name";
38
+ import DOB_FIELD from "@salesforce/schema/Patient__c.Date_of_Birth__c";
39
+
40
+ type RecordResult = { data?: Record<string, unknown>; error?: unknown };
41
+
42
+ // @ts-ignore
43
+ export default class PatientCard extends LightningElement {
44
+ // @ts-ignore
45
+ @api recordId: string;
46
+
47
+ // @ts-ignore
48
+ @wire(getRecord, { recordId: "$recordId", fields: [NAME_FIELD, DOB_FIELD] })
49
+ patient: RecordResult;
50
+
51
+ public get patientName(): string | null {
52
+ return getFieldValue(this.patient.data, NAME_FIELD) as string | null;
53
+ }
54
+
55
+ public get patientDob(): string | null {
56
+ return getFieldValue(this.patient.data, DOB_FIELD) as string | null;
57
+ }
58
+ }
59
+ ```
60
+
61
+ ```html
62
+ <!-- patientCard.html -->
63
+ <template>
64
+ <template lwc:if="{patient.data}">
65
+ <p>{patientName}</p>
66
+ <p>{patientDob}</p>
67
+ </template>
68
+ </template>
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Create a record
74
+
75
+ **Incorrect — Apex DML, counts against DML governor limit per call:**
76
+
77
+ ```typescript
78
+ // patientForm.ts
79
+ import { LightningElement } from "lwc";
80
+ import createPatient from "@salesforce/apex/PatientController.createPatient";
81
+
82
+ interface PatientFormState {
83
+ name: string;
84
+ }
85
+
86
+ // @ts-ignore
87
+ export default class PatientForm extends LightningElement {
88
+ formState: PatientFormState = { name: "" };
89
+
90
+ public handleChange(event: Event): void {
91
+ const target = event.target as HTMLInputElement;
92
+ this.formState = { ...this.formState, [target.name]: target.value };
93
+ }
94
+
95
+ public async handleSubmit(event: Event): Promise<void> {
96
+ event.preventDefault();
97
+ await createPatient({ fields: this.formState }); // ❌ bypasses LDS cache
98
+ }
99
+ }
100
+ ```
101
+
102
+ **Correct — LDS `createRecord`, platform handles DML and cache invalidation:**
103
+
104
+ ```typescript
105
+ // patientForm.ts
106
+ import { LightningElement } from "lwc";
107
+ import { createRecord } from "lightning/uiRecordApi";
108
+ import PATIENT_OBJECT from "@salesforce/schema/Patient__c";
109
+ import NAME_FIELD from "@salesforce/schema/Patient__c.Name";
110
+
111
+ interface PatientFormState {
112
+ name: string;
113
+ }
114
+
115
+ // @ts-ignore
116
+ export default class PatientForm extends LightningElement {
117
+ formState: PatientFormState = { name: "" };
118
+
119
+ public handleChange(event: Event): void {
120
+ const target = event.target as HTMLInputElement;
121
+ this.formState = { ...this.formState, [target.name]: target.value };
122
+ }
123
+
124
+ public async handleSubmit(event: Event): Promise<void> {
125
+ event.preventDefault();
126
+ const fields: Record<string, string> = {
127
+ [NAME_FIELD.fieldApiName]: this.formState.name,
128
+ };
129
+ await createRecord({ apiName: PATIENT_OBJECT.objectApiName, fields });
130
+ }
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Update a record
137
+
138
+ **Incorrect:**
139
+
140
+ ```typescript
141
+ // patientEditForm.ts
142
+ import { LightningElement, api } from "lwc";
143
+ import updatePatient from "@salesforce/apex/PatientController.updatePatient";
144
+
145
+ // @ts-ignore
146
+ export default class PatientEditForm extends LightningElement {
147
+ // @ts-ignore
148
+ @api recordId: string;
149
+ formState = { name: "" };
150
+
151
+ public handleChange(event: Event): void {
152
+ const target = event.target as HTMLInputElement;
153
+ this.formState = { ...this.formState, [target.name]: target.value };
154
+ }
155
+
156
+ public async handleSave(): Promise<void> {
157
+ await updatePatient({ recordId: this.recordId, fields: this.formState }); // ❌
158
+ }
159
+ }
160
+ ```
161
+
162
+ **Correct — `updateRecord` automatically refreshes all components holding the same record:**
163
+
164
+ ```typescript
165
+ // patientEditForm.ts
166
+ import { LightningElement, api } from "lwc";
167
+ import { updateRecord } from "lightning/uiRecordApi";
168
+ import ID_FIELD from "@salesforce/schema/Patient__c.Id";
169
+ import NAME_FIELD from "@salesforce/schema/Patient__c.Name";
170
+
171
+ // @ts-ignore
172
+ export default class PatientEditForm extends LightningElement {
173
+ // @ts-ignore
174
+ @api recordId: string;
175
+ formState = { name: "" };
176
+
177
+ public handleChange(event: Event): void {
178
+ const target = event.target as HTMLInputElement;
179
+ this.formState = { ...this.formState, [target.name]: target.value };
180
+ }
181
+
182
+ public async handleSave(): Promise<void> {
183
+ const fields: Record<string, string> = {
184
+ [ID_FIELD.fieldApiName]: this.recordId,
185
+ [NAME_FIELD.fieldApiName]: this.formState.name,
186
+ };
187
+ await updateRecord({ fields });
188
+ }
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Delete a record
195
+
196
+ **Incorrect:**
197
+
198
+ ```typescript
199
+ // patientListItem.ts
200
+ import { LightningElement, api } from "lwc";
201
+ import deletePatient from "@salesforce/apex/PatientController.deletePatient";
202
+
203
+ // @ts-ignore
204
+ export default class PatientListItem extends LightningElement {
205
+ // @ts-ignore
206
+ @api recordId: string;
207
+
208
+ public async handleDelete(): Promise<void> {
209
+ await deletePatient({ recordId: this.recordId }); // ❌
210
+ }
211
+ }
212
+ ```
213
+
214
+ **Correct:**
215
+
216
+ ```typescript
217
+ // patientListItem.ts
218
+ import { LightningElement, api } from "lwc";
219
+ import { deleteRecord } from "lightning/uiRecordApi";
220
+
221
+ // @ts-ignore
222
+ export default class PatientListItem extends LightningElement {
223
+ // @ts-ignore
224
+ @api recordId: string;
225
+
226
+ public async handleDelete(): Promise<void> {
227
+ await deleteRecord(this.recordId);
228
+ }
229
+ }
230
+ ```
231
+
232
+ ---
233
+
234
+ ## When LDS is NOT enough — fall back to Apex
235
+
236
+ Use Apex when the operation requires `WHERE`, `ORDER BY`, aggregates, joins, or complex server-side logic.
237
+
238
+ ```typescript
239
+ // appointmentList.ts — acceptable Apex fallback
240
+ import { LightningElement, api, wire } from "lwc";
241
+ import getAppointmentsByPatient from "@salesforce/apex/AppointmentController.getAppointmentsByPatient";
242
+
243
+ interface AppointmentRecord {
244
+ Id: string;
245
+ Name: string;
246
+ }
247
+
248
+ // @ts-ignore
249
+ export default class AppointmentList extends LightningElement {
250
+ // @ts-ignore
251
+ @api recordId: string;
252
+
253
+ // @ts-ignore
254
+ @wire(getAppointmentsByPatient, { patientId: "$recordId" })
255
+ appointments: { data?: AppointmentRecord[]; error?: unknown };
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ **Reference:** [lightning/uiRecordApi — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/reference-lightning-ui-api-record.html)
@@ -0,0 +1,248 @@
1
+ # data-wire-vs-imperative
2
+
3
+ **Impact: HIGH**
4
+
5
+ `@wire` and imperative Apex calls serve different purposes. Using the wrong one leads to either components that cannot react to user actions, or unnecessary re-fetches that bypass the LDS cache.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Use `@wire` when data should load automatically and stay in sync with reactive properties.
10
+ > - Use imperative calls when data fetch must be triggered explicitly by a user action.
11
+ > - After an imperative DML that affects `@wire` Apex data, call `refreshApex` to sync the wire cache.
12
+ > - Do **not** call `refreshApex` on LDS wire adapters (`getRecord`, etc.) — LDS self-invalidates after `updateRecord` / `deleteRecord`.
13
+ > - To use `refreshApex`, the wire result must be stored as a **property** (not destructured).
14
+
15
+ > **Note on import path:** `refreshApex` moved to `lightning/apex` in API v59+. Use `@salesforce/apex` for older API versions.
16
+ >
17
+ > ```typescript
18
+ > // API v59+ (LWR / newer orgs)
19
+ > import { refreshApex } from "lightning/apex";
20
+ > // API v58 and below
21
+ > import { refreshApex } from "@salesforce/apex";
22
+ > ```
23
+
24
+ ---
25
+
26
+ ## Case 1: Using `@wire` for an operation that needs to be triggered by user action
27
+
28
+ **Incorrect — trying to trigger `@wire` from a button click:**
29
+
30
+ ```typescript
31
+ // appointmentSearch.ts
32
+ import { LightningElement, wire } from "lwc";
33
+ import searchAppointments from "@salesforce/apex/AppointmentController.searchAppointments";
34
+
35
+ interface AppointmentRecord {
36
+ Id: string;
37
+ Name: string;
38
+ }
39
+
40
+ // @ts-ignore
41
+ export default class AppointmentSearch extends LightningElement {
42
+ searchTerm: string = "";
43
+
44
+ // Fires on component load — cannot be manually re-triggered.
45
+ // @ts-ignore
46
+ @wire(searchAppointments, { term: "$searchTerm" })
47
+ appointments: { data?: AppointmentRecord[]; error?: unknown };
48
+
49
+ public handleSearch(): void {
50
+ // Does nothing — @wire already ran on load.
51
+ }
52
+ }
53
+ ```
54
+
55
+ **Correct — use imperative call inside the event handler:**
56
+
57
+ ```typescript
58
+ // appointmentSearch.ts
59
+ import { LightningElement } from "lwc";
60
+ import searchAppointments from "@salesforce/apex/AppointmentController.searchAppointments";
61
+ import { reduceErrors } from "c/utils";
62
+
63
+ interface AppointmentRecord {
64
+ Id: string;
65
+ Name: string;
66
+ }
67
+
68
+ // @ts-ignore
69
+ export default class AppointmentSearch extends LightningElement {
70
+ searchTerm: string = "";
71
+ appointments: AppointmentRecord[] = [];
72
+ isLoading: boolean = false;
73
+ error: unknown = null;
74
+
75
+ public handleChange(event: Event): void {
76
+ this.searchTerm = (event.target as HTMLInputElement).value;
77
+ }
78
+
79
+ public async handleSearch(): Promise<void> {
80
+ this.isLoading = true;
81
+ this.error = null;
82
+ try {
83
+ this.appointments = await searchAppointments({ term: this.searchTerm });
84
+ } catch (e) {
85
+ this.error = e;
86
+ } finally {
87
+ this.isLoading = false;
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Case 2: Using imperative call in `connectedCallback` when `@wire` would suffice
96
+
97
+ **Incorrect — imperative in `connectedCallback`, does not react to `recordId` changes:**
98
+
99
+ ```typescript
100
+ // patientSummary.ts
101
+ import { LightningElement, api } from "lwc";
102
+ import getPatientSummary from "@salesforce/apex/PatientController.getPatientSummary";
103
+
104
+ interface PatientSummaryRecord {
105
+ Name: string;
106
+ }
107
+
108
+ // @ts-ignore
109
+ export default class PatientSummary extends LightningElement {
110
+ // @ts-ignore
111
+ @api recordId: string;
112
+ summary: PatientSummaryRecord | null = null;
113
+
114
+ // Runs once on mount. Does not re-run if recordId changes.
115
+ // Bypasses LDS cache — always fires a new SOQL query.
116
+ public async connectedCallback(): Promise<void> {
117
+ this.summary = await getPatientSummary({ recordId: this.recordId });
118
+ }
119
+ }
120
+ ```
121
+
122
+ **Correct — `@wire` reacts to `recordId` changes and uses LDS cache:**
123
+
124
+ ```typescript
125
+ // patientSummary.ts
126
+ import { LightningElement, api, wire } from "lwc";
127
+ import getPatientSummary from "@salesforce/apex/PatientController.getPatientSummary";
128
+
129
+ interface PatientSummaryRecord {
130
+ Name: string;
131
+ }
132
+
133
+ // @ts-ignore
134
+ export default class PatientSummary extends LightningElement {
135
+ // @ts-ignore
136
+ @api recordId: string;
137
+
138
+ // @ts-ignore
139
+ @wire(getPatientSummary, { recordId: "$recordId" })
140
+ summary: { data?: PatientSummaryRecord; error?: unknown };
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Case 3: Syncing `@wire` Apex data after imperative DML — `refreshApex`
147
+
148
+ **Incorrect — wire result destructured, `refreshApex` cannot be called:**
149
+
150
+ ```typescript
151
+ // appointmentList.ts
152
+ import { LightningElement, api, wire } from "lwc";
153
+ import { refreshApex } from "lightning/apex";
154
+ import getAppointments from "@salesforce/apex/AppointmentController.getAppointments";
155
+ import deleteAppointment from "@salesforce/apex/AppointmentController.deleteAppointment";
156
+
157
+ // @ts-ignore
158
+ export default class AppointmentList extends LightningElement {
159
+ // @ts-ignore
160
+ @api recordId: string;
161
+ appointments: unknown[] = [];
162
+
163
+ // ❌ Destructured — refreshApex has nothing to reference
164
+ // @ts-ignore
165
+ @wire(getAppointments, { recordId: "$recordId" })
166
+ public handleAppointments({ data, error }: { data?: unknown[]; error?: unknown }): void {
167
+ this.appointments = data ?? [];
168
+ }
169
+
170
+ public async handleDelete(event: Event): Promise<void> {
171
+ const target = event.target as HTMLElement;
172
+ await deleteAppointment({ appointmentId: target.dataset.id });
173
+ // ❌ Cannot call refreshApex — no wire result property stored
174
+ await refreshApex(this.appointments);
175
+ }
176
+ }
177
+ ```
178
+
179
+ **Correct — store the full wire result, call `refreshApex` after DML:**
180
+
181
+ ```typescript
182
+ // appointmentList.ts
183
+ import { LightningElement, api, wire } from "lwc";
184
+ import { refreshApex } from "lightning/apex";
185
+ import { ShowToastEvent } from "lightning/platformShowToastEvent";
186
+ import getAppointments from "@salesforce/apex/AppointmentController.getAppointments";
187
+ import deleteAppointment from "@salesforce/apex/AppointmentController.deleteAppointment";
188
+ import { reduceErrors } from "c/utils";
189
+
190
+ interface AppointmentRecord {
191
+ Id: string;
192
+ Name: string;
193
+ }
194
+
195
+ type WireResult<T> = { data?: T; error?: unknown };
196
+
197
+ // @ts-ignore
198
+ export default class AppointmentList extends LightningElement {
199
+ // @ts-ignore
200
+ @api recordId: string;
201
+
202
+ // ✅ Store full wire result — refreshApex needs the whole object
203
+ wiredAppointments: WireResult<AppointmentRecord[]> = {};
204
+
205
+ public get appointments(): AppointmentRecord[] {
206
+ return this.wiredAppointments?.data ?? [];
207
+ }
208
+
209
+ // @ts-ignore
210
+ @wire(getAppointments, { recordId: "$recordId" })
211
+ public handleAppointments(result: WireResult<AppointmentRecord[]>): void {
212
+ this.wiredAppointments = result; // ✅ keep reference for refreshApex
213
+ }
214
+
215
+ public async handleDelete(event: Event): Promise<void> {
216
+ const target = event.target as HTMLElement;
217
+ try {
218
+ await deleteAppointment({ appointmentId: target.dataset.id });
219
+ await refreshApex(this.wiredAppointments); // ✅ pass the stored wire result
220
+ } catch (error) {
221
+ this.dispatchEvent(
222
+ new ShowToastEvent({
223
+ title: "Delete failed",
224
+ message: reduceErrors(error).join(", "),
225
+ variant: "error",
226
+ }),
227
+ );
228
+ }
229
+ }
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Quick Decision Guide
236
+
237
+ | Situation | Use |
238
+ | ------------------------------------------------------------ | ------------------------------------ |
239
+ | Data loads when component mounts | `@wire` |
240
+ | Data depends on a reactive property (`$recordId`) | `@wire` |
241
+ | Fetch triggered by button click or form submit | Imperative |
242
+ | Operation has side effects (send email, run calculation) | Imperative |
243
+ | Apex `@wire` data stale after imperative DML | `refreshApex(this.wiredResult)` |
244
+ | LDS `@wire` data stale after `updateRecord` / `deleteRecord` | ❌ Not needed — LDS self-invalidates |
245
+
246
+ ---
247
+
248
+ **Reference:** [Call Apex Methods — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/apex.html)