sf-lwc-development

LWC development — components, reactive properties, wire service, Apex integration, events, lifecycle hooks. Use when building LWC components or debugging wire/reactivity. Do NOT use for Aura, Visualforce, or Flow.

LWC Development

Lightning Web Components (LWC) is Salesforce's modern component model based on web standards — Custom Elements, Shadow DOM, and ES modules.

When to Use

  • When building new Lightning Web Components from scratch
  • When migrating Aura components to LWC
  • When debugging reactive property or wire service issues
  • When implementing parent-child communication with events or @api
  • When adding LWC components to record pages, app pages, or Experience Cloud sites

@../_reference/LWC_PATTERNS.md


Component Creation Procedure

Every LWC component is a folder with at minimum an HTML template and a JavaScript class.

force-app/main/default/lwc/
  accountList/
    accountList.html          <- Template
    accountList.js            <- Component class
    accountList.css           <- Component-scoped styles (optional)
    accountList.js-meta.xml   <- Metadata (targets, properties)

Step 1 — HTML Template

<template>
    <lightning-card title="Accounts" icon-name="standard:account">
        <template lwc:if={isLoading}>
            <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
        </template>
        <template lwc:elseif={hasError}>
            <p class="slds-text-color_error">{errorMessage}</p>
        </template>
        <template lwc:else>
            <template for:each={accounts} for:item="account">
                <div key={account.Id} class="account-row">
                    <span>{account.Name}</span>
                    <lightning-button label="View" data-id={account.Id}
                        onclick={handleViewAccount}></lightning-button>
                </div>
            </template>
        </template>
    </lightning-card>
</template>

Step 2 — JavaScript Class

import { LightningElement, api, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { NavigationMixin } from 'lightning/navigation';
import getAccounts from '@salesforce/apex/AccountsController.getAccounts';

export default class AccountList extends NavigationMixin(LightningElement) {
    @api recordId;
    @api maxRecords = 10;

    accounts = [];
    isLoading = false;
    error;

    get hasError() { return this.error !== undefined; }
    get isEmpty() { return !this.isLoading && this.accounts.length === 0; }
    get errorMessage() {
        return this.error?.body?.message ?? this.error?.message ?? 'An unknown error occurred.';
    }

    connectedCallback() { this.loadAccounts(); }

    handleViewAccount(event) {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: { recordId: event.currentTarget.dataset.id,
                          objectApiName: 'Account', actionName: 'view' }
        });
    }

    async loadAccounts() {
        this.isLoading = true;
        this.error = undefined;
        try {
            this.accounts = await getAccounts({
                searchTerm: this.searchTerm, maxRecords: this.maxRecords
            });
        } catch (error) {
            this.error = error;
        } finally {
            this.isLoading = false;
        }
    }
}

Step 3 — Meta XML

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>66.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <property name="maxRecords" type="Integer" default="10"
                      label="Maximum Records to Display" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Wire Service Usage

The wire service declaratively connects components to Salesforce data and re-runs when reactive parameters change.

Wire with Apex

import { LightningElement, wire, api } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getAccountDetails from '@salesforce/apex/AccountsController.getAccountDetails';

export default class AccountDetails extends LightningElement {
    @api recordId;
    _wiredResult;

    @wire(getAccountDetails, { accountId: '$recordId' })
    wiredAccount(result) {
        this._wiredResult = result;
        if (result.data) {
            this.account = result.data;
            this.error = undefined;
        } else if (result.error) {
            this.error = result.error;
            this.account = undefined;
        }
    }

    async handleSave(event) {
        await updateAccount({ accountId: this.recordId, fields: event.detail.fields });
        await refreshApex(this._wiredResult);
    }
}

Wire with Lightning Data Service

import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue, updateRecord } from 'lightning/uiRecordApi';
import ACCOUNT_NAME from '@salesforce/schema/Account.Name';
import ACCOUNT_INDUSTRY from '@salesforce/schema/Account.Industry';

export default class AccountHeader extends LightningElement {
    @api recordId;

    @wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME, ACCOUNT_INDUSTRY] })
    account;

    get name() { return getFieldValue(this.account.data, ACCOUNT_NAME); }
    get industry() { return getFieldValue(this.account.data, ACCOUNT_INDUSTRY); }
}

Event Handling — Component Communication

Child to Parent: Custom Events

// child: opportunityCard.js
handleSelect() {
    this.dispatchEvent(new CustomEvent('opportunityselect', {
        detail: { opportunityId: this.opportunity.Id },
        bubbles: false, composed: false
    }));
}
<!-- parent template -->
<c-opportunity-card opportunity={opp}
    onopportunityselect={handleOpportunitySelect}></c-opportunity-card>

Cross-Component: Lightning Message Service

import { publish, subscribe, unsubscribe, MessageContext, APPLICATION_SCOPE }
    from 'lightning/messageService';
import CHANNEL from '@salesforce/messageChannel/OpportunitySelected__c';

// Publisher
@wire(MessageContext) messageContext;
handleSelect(event) {
    publish(this.messageContext, CHANNEL, { opportunityId: event.target.dataset.id });
}

// Subscriber
connectedCallback() {
    this.subscription = subscribe(this.messageContext, CHANNEL,
        (msg) => this.handleMessage(msg), { scope: APPLICATION_SCOPE });
}
disconnectedCallback() { unsubscribe(this.subscription); }

Slots — Composition Patterns

<!-- child: modalWrapper.html -->
<template>
    <div class="modal-header"><slot name="header"><h2>Default Header</h2></slot></div>
    <div class="modal-body"><slot></slot></div>
    <div class="modal-footer"><slot name="footer"></slot></div>
</template>

<!-- parent usage -->
<c-modal-wrapper>
    <span slot="header">Edit Account</span>
    <lightning-record-edit-form record-id={recordId} object-api-name="Account">
        <lightning-input-field field-name="Name"></lightning-input-field>
    </lightning-record-edit-form>
    <div slot="footer">
        <lightning-button label="Save" variant="brand" onclick={handleSave}></lightning-button>
    </div>
</c-modal-wrapper>

Light DOM

Renders component markup directly into the parent DOM (no shadow boundary).

export default class ThemedComponent extends LightningElement {
    static renderMode = 'light';
}

Use for: global CSS theming, third-party library integration, Experience Cloud sites, simple leaf components. Query with this.querySelector() instead of this.template.querySelector().


Spring '26 Features

SLDS 2.0

Use --slds-c-* styling hooks (replaces --lwc-* design tokens). Run npx slds-lint to check compliance.

TypeScript Support

npm install --save-dev @salesforce/lightning-types

Complex Template Expressions (Beta)

<p>{account.Name ?? 'Unknown Account'}</p>
<p>{formatRevenue(account.AnnualRevenue)}</p>

Use getters for production code until this reaches GA.

LWC in Screen Flows

Expose components as Flow screen actions via lightning__FlowScreen target. Implement @api validate() for Flow navigation validation.


Related

Guardrails

  • sf-lwc-constraints — Enforced rules for LWC naming, reactivity, security, and accessibility

Agents

  • sf-lwc-agent — For interactive, in-depth LWC review guidance