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,202 @@
1
+ # template-dom-querying
2
+
3
+ **Impact: HIGH**
4
+
5
+ DOM querying in LWC is scoped to `this.template`. You cannot reach into a child component's DOM from a parent — attempting to do so always returns `null`.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Always use `data-id` attributes as query selectors — never class names or tag names.
10
+ > - `this.template.querySelector` is safe inside event handlers and `renderedCallback` — **not** in `connectedCallback`.
11
+ > - Never query DOM across component boundaries. Expose `@api` methods on children instead.
12
+
13
+ ---
14
+
15
+ ## Case 1: Querying across component boundaries
16
+
17
+ **Incorrect — parent queries into child's shadow DOM (always returns null):**
18
+
19
+ ```typescript
20
+ // patientOnboarding.ts
21
+ import { LightningElement } from "lwc";
22
+
23
+ // @ts-ignore
24
+ export default class PatientOnboarding extends LightningElement {
25
+ currentStep: number = 1;
26
+
27
+ public handleNext(): void {
28
+ // ❌ Always returns null — shadow DOM blocks cross-component queries
29
+ const input = this.template.querySelector("c-patient-onboarding-step1 lightning-input");
30
+ if (!input) return;
31
+ this.currentStep = 2;
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Correct — parent calls `@api` method on child, child validates its own DOM:**
37
+
38
+ ```typescript
39
+ // patientOnboardingStep1.ts
40
+ import { LightningElement, api } from "lwc";
41
+
42
+ // @ts-ignore
43
+ export default class PatientOnboardingStep1 extends LightningElement {
44
+ // @ts-ignore
45
+ @api
46
+ public validate(): boolean {
47
+ const inputs = this.template.querySelectorAll<HTMLInputElement>("lightning-input");
48
+ return [...inputs].reduce((isValid, input) => {
49
+ input.reportValidity();
50
+ return isValid && input.checkValidity();
51
+ }, true);
52
+ }
53
+ }
54
+ ```
55
+
56
+ ```typescript
57
+ // patientOnboarding.ts
58
+ import { LightningElement } from "lwc";
59
+
60
+ interface StepComponent extends HTMLElement {
61
+ validate(): boolean;
62
+ }
63
+
64
+ // @ts-ignore
65
+ export default class PatientOnboarding extends LightningElement {
66
+ currentStep: number = 1;
67
+
68
+ public get isStep1(): boolean {
69
+ return this.currentStep === 1;
70
+ }
71
+ public get isStep2(): boolean {
72
+ return this.currentStep === 2;
73
+ }
74
+
75
+ public handleNext(): void {
76
+ const stepComponent = this.template.querySelector<StepComponent>('[data-id="current-step"]');
77
+ if (!stepComponent) return;
78
+
79
+ // ✅ Child validates itself — parent only asks for the result
80
+ if (!stepComponent.validate()) return;
81
+
82
+ this.currentStep += 1;
83
+ }
84
+
85
+ public handlePrev(): void {
86
+ this.currentStep -= 1;
87
+ }
88
+ }
89
+ ```
90
+
91
+ ```html
92
+ <!-- patientOnboarding.html -->
93
+ <template>
94
+ <template lwc:if="{isStep1}">
95
+ <c-patient-onboarding-step1 data-id="current-step"></c-patient-onboarding-step1>
96
+ </template>
97
+ <template lwc:if="{isStep2}">
98
+ <c-patient-onboarding-step2 data-id="current-step"></c-patient-onboarding-step2>
99
+ </template>
100
+ <div>
101
+ <lightning-button label="Back" onclick="{handlePrev}"></lightning-button>
102
+ <lightning-button variant="brand" label="Next" onclick="{handleNext}"></lightning-button>
103
+ </div>
104
+ </template>
105
+ ```
106
+
107
+ > `data-id="current-step"` stays the same across all steps — parent always queries the same selector.
108
+
109
+ ---
110
+
111
+ ## Case 2: Class/tag selectors instead of `data-id`
112
+
113
+ **Incorrect — fragile selectors tied to implementation details:**
114
+
115
+ ```typescript
116
+ // bookingForm.ts
117
+ handleSubmit(event: Event): void {
118
+ event.preventDefault();
119
+ // ❌ Breaks if class name changes or element type is swapped
120
+ const input = this.template.querySelector<HTMLInputElement>('.date-input');
121
+ }
122
+ ```
123
+
124
+ **Correct — stable `data-id` selectors:**
125
+
126
+ ```typescript
127
+ // bookingForm.ts
128
+ import { LightningElement } from "lwc";
129
+
130
+ // @ts-ignore
131
+ export default class BookingForm extends LightningElement {
132
+ public handleSubmit(event: Event): void {
133
+ event.preventDefault();
134
+
135
+ const dateInput = this.template.querySelector<HTMLInputElement>('[data-id="date-input"]');
136
+ const timeInput = this.template.querySelector<HTMLInputElement>('[data-id="time-input"]');
137
+
138
+ if (!dateInput || !timeInput) return;
139
+
140
+ if (!dateInput.checkValidity() || !timeInput.checkValidity()) {
141
+ dateInput.reportValidity();
142
+ timeInput.reportValidity();
143
+ return;
144
+ }
145
+
146
+ this.submitBooking();
147
+ }
148
+
149
+ private submitBooking(): void {
150
+ // submit logic
151
+ }
152
+ }
153
+ ```
154
+
155
+ ```html
156
+ <!-- bookingForm.html -->
157
+ <template>
158
+ <form onsubmit="{handleSubmit}">
159
+ <lightning-input data-id="date-input" name="date" type="date" label="Appointment Date" required></lightning-input>
160
+ <lightning-input data-id="time-input" name="time" type="time" label="Appointment Time" required></lightning-input>
161
+ <lightning-button type="submit" variant="brand" label="Book"></lightning-button>
162
+ </form>
163
+ </template>
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Case 3: Querying multiple elements with `querySelectorAll`
169
+
170
+ ```typescript
171
+ // patientOnboardingStep2.ts
172
+ import { LightningElement, api } from "lwc";
173
+
174
+ // @ts-ignore
175
+ export default class PatientOnboardingStep2 extends LightningElement {
176
+ // @ts-ignore
177
+ @api
178
+ public validate(): boolean {
179
+ const allInputs = this.template.querySelectorAll<HTMLInputElement>("lightning-input, lightning-textarea");
180
+ return [...allInputs].reduce((isValid, input) => {
181
+ input.reportValidity();
182
+ return isValid && input.checkValidity();
183
+ }, true);
184
+ }
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Quick Decision Guide
191
+
192
+ | Situation | Pattern |
193
+ | -------------------------------------- | ------------------------------------------------------------ |
194
+ | Query element in the same component | `this.template.querySelector<T>('[data-id="x"]')` |
195
+ | Safe timing for querySelector | Event handlers, `renderedCallback` — not `connectedCallback` |
196
+ | Trigger behavior in a child | Expose `@api` method on child, call from parent |
197
+ | Validate all inputs in a form | `querySelectorAll('lightning-input')` + `reduce` |
198
+ | Query child's internal DOM from parent | ❌ Not possible — Shadow DOM blocks it |
199
+
200
+ ---
201
+
202
+ **Reference:** [Access Elements the Component Owns — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/create-components-dom-work.html)
@@ -0,0 +1,285 @@
1
+ # template-form-pattern
2
+
3
+ **Impact: HIGH**
4
+
5
+ LWC provides `lightning-record-edit-form` for standard CRUD forms. Only build a custom form when the UX requirement goes beyond what the base component supports.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Use `lightning-record-edit-form` for simple create/edit of a single record.
10
+ > - In custom forms, use a single `handleChange` keyed on `event.target.name` — never per-field handlers.
11
+ > - Keep all form state in one typed interface.
12
+ > - In multi-step wizards, the parent owns state and submits once at the final step. Each step exposes `validate()` and `getFormData()` as `@api` methods.
13
+
14
+ ---
15
+
16
+ ## When to use `lightning-record-edit-form`
17
+
18
+ ```html
19
+ <!-- appointmentEditForm.html — no JS needed -->
20
+ <template>
21
+ <lightning-record-edit-form record-id="{recordId}" object-api-name="Appointment__c">
22
+ <lightning-messages></lightning-messages>
23
+ <lightning-input-field field-name="Name"></lightning-input-field>
24
+ <lightning-input-field field-name="Date__c"></lightning-input-field>
25
+ <lightning-input-field field-name="Status__c"></lightning-input-field>
26
+ <lightning-button type="submit" variant="brand" label="Save"></lightning-button>
27
+ </lightning-record-edit-form>
28
+ </template>
29
+ ```
30
+
31
+ ---
32
+
33
+ ## When to use a custom form
34
+
35
+ | Situation | Reason |
36
+ | ------------------------------------ | ---------------------------------------------- |
37
+ | Multi-step wizard | `lightning-record-edit-form` cannot span steps |
38
+ | Conditional fields | Requires reactive state control |
39
+ | Data from multiple objects | LDS handles one object at a time |
40
+ | Custom validation beyond field-level | Needs manual `checkValidity` |
41
+
42
+ ---
43
+
44
+ ## Case 1: Conditional fields — single `handleChange`, centralized state
45
+
46
+ **Incorrect — per-field handlers, scattered booleans:**
47
+
48
+ ```typescript
49
+ // referralForm.ts
50
+ import { LightningElement } from "lwc";
51
+
52
+ // @ts-ignore
53
+ export default class ReferralForm extends LightningElement {
54
+ referralType: string = "";
55
+ specialistName: string = "";
56
+ clinicName: string = "";
57
+ showSpecialistFields: boolean = false;
58
+
59
+ // ❌ Grows linearly with form size
60
+ public handleTypeChange(event: Event): void {
61
+ this.referralType = (event.target as HTMLInputElement).value;
62
+ this.showSpecialistFields = this.referralType === "specialist";
63
+ }
64
+
65
+ public handleSpecialistChange(event: Event): void {
66
+ this.specialistName = (event.target as HTMLInputElement).value;
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Correct — single `handleChange`, derived conditional from state:**
72
+
73
+ ```typescript
74
+ // referralForm.ts
75
+ import { LightningElement } from "lwc";
76
+
77
+ interface ReferralFormState {
78
+ referralType: string;
79
+ specialistName: string;
80
+ clinicName: string;
81
+ notes: string;
82
+ }
83
+
84
+ // @ts-ignore
85
+ export default class ReferralForm extends LightningElement {
86
+ formState: ReferralFormState = {
87
+ referralType: "",
88
+ specialistName: "",
89
+ clinicName: "",
90
+ notes: "",
91
+ };
92
+
93
+ // ✅ One handler for all fields
94
+ public handleChange(event: Event): void {
95
+ const target = event.target as HTMLInputElement;
96
+ this.formState = { ...this.formState, [target.name]: target.value };
97
+ }
98
+
99
+ // ✅ Derived from state — no extra boolean needed
100
+ public get showSpecialistFields(): boolean {
101
+ return this.formState.referralType === "specialist";
102
+ }
103
+
104
+ public handleSubmit(event: Event): void {
105
+ event.preventDefault();
106
+ const allInputs = this.template.querySelectorAll<HTMLInputElement>("lightning-input, lightning-combobox");
107
+ const isValid = [...allInputs].reduce((valid, input) => {
108
+ input.reportValidity();
109
+ return valid && input.checkValidity();
110
+ }, true);
111
+
112
+ if (!isValid) return;
113
+ this.submitForm();
114
+ }
115
+
116
+ private submitForm(): void {
117
+ // submit logic
118
+ }
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Case 2: Multi-step wizard — parent owns state, submits once at final step
125
+
126
+ **Step component — renders fields, exposes `validate()` and `getFormData()`:**
127
+
128
+ ```typescript
129
+ // onboardingStep1.ts
130
+ import { LightningElement, api } from "lwc";
131
+
132
+ interface Step1FormState {
133
+ firstName: string;
134
+ lastName: string;
135
+ email: string;
136
+ }
137
+
138
+ // @ts-ignore
139
+ export default class OnboardingStep1 extends LightningElement {
140
+ formState: Step1FormState = {
141
+ firstName: "",
142
+ lastName: "",
143
+ email: "",
144
+ };
145
+
146
+ public handleChange(event: Event): void {
147
+ const target = event.target as HTMLInputElement;
148
+ this.formState = { ...this.formState, [target.name]: target.value };
149
+ }
150
+
151
+ // @ts-ignore
152
+ @api
153
+ public validate(): boolean {
154
+ const inputs = this.template.querySelectorAll<HTMLInputElement>("lightning-input");
155
+ return [...inputs].reduce((valid, input) => {
156
+ input.reportValidity();
157
+ return valid && input.checkValidity();
158
+ }, true);
159
+ }
160
+
161
+ // @ts-ignore
162
+ @api
163
+ public getFormData(): Step1FormState {
164
+ return { ...this.formState };
165
+ }
166
+ }
167
+ ```
168
+
169
+ **Parent — accumulates state, submits once at final step:**
170
+
171
+ ```typescript
172
+ // onboardingWizard.ts
173
+ import { LightningElement } from "lwc";
174
+ import { createRecord } from "lightning/uiRecordApi";
175
+ import { ShowToastEvent } from "lightning/platformShowToastEvent";
176
+ import { reduceErrors } from "c/utils";
177
+ import CONTACT_OBJECT from "@salesforce/schema/Contact";
178
+
179
+ interface StepComponent extends HTMLElement {
180
+ validate(): boolean;
181
+ getFormData(): Record<string, string>;
182
+ }
183
+
184
+ // @ts-ignore
185
+ export default class OnboardingWizard extends LightningElement {
186
+ currentStep: number = 1;
187
+ totalSteps: number = 3;
188
+ formData: Record<string, string> = {};
189
+
190
+ public get isLastStep(): boolean {
191
+ return this.currentStep === this.totalSteps;
192
+ }
193
+
194
+ public get isStep1(): boolean {
195
+ return this.currentStep === 1;
196
+ }
197
+ public get isStep2(): boolean {
198
+ return this.currentStep === 2;
199
+ }
200
+ public get isStep3(): boolean {
201
+ return this.currentStep === 3;
202
+ }
203
+ public get showPrev(): boolean {
204
+ return this.currentStep > 1;
205
+ }
206
+ public get nextLabel(): string {
207
+ return this.isLastStep ? "Submit" : "Next";
208
+ }
209
+
210
+ public async handleNext(): Promise<void> {
211
+ const stepComponent = this.template.querySelector<StepComponent>('[data-id="current-step"]');
212
+ if (!stepComponent) return;
213
+
214
+ if (!stepComponent.validate()) return;
215
+
216
+ this.formData = { ...this.formData, ...stepComponent.getFormData() };
217
+
218
+ if (this.isLastStep) {
219
+ await this.submitAll();
220
+ return;
221
+ }
222
+
223
+ this.currentStep += 1;
224
+ }
225
+
226
+ public handlePrev(): void {
227
+ this.currentStep -= 1;
228
+ }
229
+
230
+ private async submitAll(): Promise<void> {
231
+ try {
232
+ await createRecord({
233
+ apiName: CONTACT_OBJECT.objectApiName,
234
+ fields: this.formData,
235
+ });
236
+ this.dispatchEvent(new ShowToastEvent({ title: "Success", message: "Onboarding complete.", variant: "success" }));
237
+ } catch (error) {
238
+ this.dispatchEvent(
239
+ new ShowToastEvent({
240
+ title: "Submission failed",
241
+ message: reduceErrors(error).join(", "),
242
+ variant: "error",
243
+ }),
244
+ );
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
250
+ ```html
251
+ <!-- onboardingWizard.html -->
252
+ <template>
253
+ <template lwc:if="{isStep1}">
254
+ <c-onboarding-step1 data-id="current-step"></c-onboarding-step1>
255
+ </template>
256
+ <template lwc:if="{isStep2}">
257
+ <c-onboarding-step2 data-id="current-step"></c-onboarding-step2>
258
+ </template>
259
+ <template lwc:if="{isStep3}">
260
+ <c-onboarding-step3 data-id="current-step"></c-onboarding-step3>
261
+ </template>
262
+
263
+ <div>
264
+ <template lwc:if="{showPrev}">
265
+ <lightning-button label="Back" onclick="{handlePrev}"></lightning-button>
266
+ </template>
267
+ <lightning-button variant="brand" label="{nextLabel}" onclick="{handleNext}"></lightning-button>
268
+ </div>
269
+ </template>
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Quick Decision Guide
275
+
276
+ | Situation | Pattern |
277
+ | -------------------------------- | ------------------------------------------------------------------- |
278
+ | Simple create/edit single record | `lightning-record-edit-form` |
279
+ | Conditional fields | Custom form + `get` derived from `formState` |
280
+ | Multi-step wizard | Parent owns `formData`; step exposes `validate()` + `getFormData()` |
281
+ | Per-field change handlers | ❌ Use single `handleChange` keyed on `name` |
282
+
283
+ ---
284
+
285
+ **Reference:** [Build Custom UI to Create and Edit Records — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/data-salesforce-write.html)
@@ -0,0 +1,206 @@
1
+ # template-track-immutable
2
+
3
+ **Impact: MEDIUM**
4
+
5
+ `@track` was required in early LWC to make object/array properties reactive. Since Spring '20, all properties are tracked deeply by default. The correct way to trigger reactivity is through immutable updates.
6
+
7
+ > **Rule:**
8
+ >
9
+ > - Do not use `@track` — it is no longer needed.
10
+ > - Trigger reactivity on objects: `this.obj = { ...this.obj, key: value }`.
11
+ > - Trigger reactivity on arrays: `[...arr, newItem]`, `arr.filter(...)`, `arr.map(...)`.
12
+ > - Never mutate an object or array in place and expect the template to re-render.
13
+
14
+ ---
15
+
16
+ ## Case 1: Unnecessary `@track`, in-place mutation
17
+
18
+ **Incorrect:**
19
+
20
+ ```typescript
21
+ // appointmentForm.ts
22
+ import { LightningElement, track } from "lwc";
23
+
24
+ interface AppointmentFormState {
25
+ date: string;
26
+ time: string;
27
+ notes: string;
28
+ }
29
+
30
+ // @ts-ignore
31
+ export default class AppointmentForm extends LightningElement {
32
+ // @ts-ignore
33
+ @track isLoading: boolean = false; // ❌ unnecessary
34
+ // @ts-ignore
35
+ @track errorMessage: string = ""; // ❌ unnecessary
36
+ // @ts-ignore
37
+ @track formState: AppointmentFormState = { date: "", time: "", notes: "" };
38
+
39
+ public handleChange(event: Event): void {
40
+ const target = event.target as HTMLInputElement;
41
+ // ❌ Mutates in place — template may not re-render reliably
42
+ (this.formState as Record<string, string>)[target.name] = target.value;
43
+ }
44
+ }
45
+ ```
46
+
47
+ **Correct — no `@track`, immutable update:**
48
+
49
+ ```typescript
50
+ // appointmentForm.ts
51
+ import { LightningElement } from "lwc";
52
+
53
+ interface AppointmentFormState {
54
+ date: string;
55
+ time: string;
56
+ notes: string;
57
+ }
58
+
59
+ // @ts-ignore
60
+ export default class AppointmentForm extends LightningElement {
61
+ isLoading: boolean = false;
62
+ errorMessage: string = "";
63
+ formState: AppointmentFormState = { date: "", time: "", notes: "" };
64
+
65
+ public handleChange(event: Event): void {
66
+ const target = event.target as HTMLInputElement;
67
+ // ✅ New object reference — LWC detects the change and re-renders
68
+ this.formState = { ...this.formState, [target.name]: target.value };
69
+ }
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Case 2: Mutating an array in place
76
+
77
+ **Incorrect — push/splice mutates the existing reference, no re-render:**
78
+
79
+ ```typescript
80
+ // appointmentList.ts
81
+ import { LightningElement } from "lwc";
82
+
83
+ interface AppointmentRecord {
84
+ Id: string;
85
+ Name: string;
86
+ }
87
+
88
+ // @ts-ignore
89
+ export default class AppointmentList extends LightningElement {
90
+ appointments: AppointmentRecord[] = [];
91
+
92
+ public handleAdd(newAppointment: AppointmentRecord): void {
93
+ this.appointments.push(newAppointment); // ❌ mutates in place
94
+ }
95
+
96
+ public handleRemove(appointmentId: string): void {
97
+ const index = this.appointments.findIndex((a) => a.Id === appointmentId);
98
+ this.appointments.splice(index, 1); // ❌ mutates in place
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Correct — new array reference:**
104
+
105
+ ```typescript
106
+ // appointmentList.ts
107
+ import { LightningElement } from "lwc";
108
+
109
+ interface AppointmentRecord {
110
+ Id: string;
111
+ Name: string;
112
+ }
113
+
114
+ // @ts-ignore
115
+ export default class AppointmentList extends LightningElement {
116
+ appointments: AppointmentRecord[] = [];
117
+
118
+ public handleAdd(newAppointment: AppointmentRecord): void {
119
+ this.appointments = [...this.appointments, newAppointment]; // ✅
120
+ }
121
+
122
+ public handleRemove(appointmentId: string): void {
123
+ this.appointments = this.appointments.filter((a) => a.Id !== appointmentId); // ✅
124
+ }
125
+
126
+ public handleUpdate(updatedAppointment: AppointmentRecord): void {
127
+ this.appointments = this.appointments.map((a) =>
128
+ a.Id === updatedAppointment.Id ? { ...a, ...updatedAppointment } : a,
129
+ ); // ✅
130
+ }
131
+
132
+ public handleRemoveClick(event: Event): void {
133
+ const target = event.target as HTMLElement;
134
+ this.handleRemove(target.dataset.id ?? "");
135
+ }
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Case 3: Updating a nested object property
142
+
143
+ **Incorrect — mutating nested property:**
144
+
145
+ ```typescript
146
+ // profileForm.ts
147
+ public handleAddressChange(event: Event): void {
148
+ const target = event.target as HTMLInputElement;
149
+ // ❌ Mutates nested object — no re-render
150
+ this.formState.address.city = target.value;
151
+ }
152
+ ```
153
+
154
+ **Correct — spread at every changed level:**
155
+
156
+ ```typescript
157
+ // profileForm.ts
158
+ import { LightningElement } from "lwc";
159
+
160
+ interface Address {
161
+ city: string;
162
+ street: string;
163
+ }
164
+
165
+ interface ProfileFormState {
166
+ name: string;
167
+ address: Address;
168
+ }
169
+
170
+ // @ts-ignore
171
+ export default class ProfileForm extends LightningElement {
172
+ formState: ProfileFormState = {
173
+ name: "",
174
+ address: { city: "", street: "" },
175
+ };
176
+
177
+ public handleAddressChange(event: Event): void {
178
+ const target = event.target as HTMLInputElement;
179
+ this.formState = {
180
+ ...this.formState,
181
+ address: {
182
+ ...this.formState.address,
183
+ [target.name]: target.value,
184
+ },
185
+ };
186
+ }
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Quick Decision Guide
193
+
194
+ | Situation | Pattern |
195
+ | ----------------------------- | ------------------------------------------ |
196
+ | Update a primitive property | Direct assignment: `this.value = newValue` |
197
+ | Update one field in an object | `this.obj = { ...this.obj, field: value }` |
198
+ | Add item to array | `this.arr = [...this.arr, newItem]` |
199
+ | Remove item from array | `this.arr = this.arr.filter(...)` |
200
+ | Update item in array | `this.arr = this.arr.map(...)` |
201
+ | Update nested object | Spread at every changed level |
202
+ | Should I use `@track`? | No |
203
+
204
+ ---
205
+
206
+ **Reference:** [Reactivity — Salesforce LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide/reactivity.html)