home Home build Tools bug_report Errors menu_book Guides lightbulb Tips smart_toy Prompts extension Extensions folder_open Resources info About
search

Lightning Web Components (LWC) is Salesforce's modern UI framework built on web standards — custom elements, shadow DOM, and ES modules. Unlike Aura, LWC uses native browser APIs, making it significantly faster and easier to maintain.

Component Anatomy

Every LWC component is a folder with at minimum three files: an HTML template, a JavaScript controller, and an XML metadata file. The folder name defines the component's tag name.

HTML — accountCard.html
<!-- accountCard/accountCard.html -->
<template>
    <lightning-card title={account.Name} icon-name="standard:account">
        <div class="slds-card__body slds-card__body_inner">

            <!-- Conditional rendering -->
            <template lwc:if={isLoaded}>
                <p>Industry: <strong>{account.Industry}</strong></p>
                <p>Revenue: <strong>{formattedRevenue}</strong></p>
            </template>

            <template lwc:else>
                <lightning-spinner alternative-text="Loading"></lightning-spinner>
            </template>

            <!-- Iterating lists -->
            <template for:each={contacts} for:item="contact">
                <p key={contact.Id}>{contact.Name}</p>
            </template>

        </div>
        <div slot="footer">
            <lightning-button label="Edit" onclick={handleEdit}></lightning-button>
        </div>
    </lightning-card>
</template>
JavaScript — accountCard.js
import { LightningElement, api, track, wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import getAccountDetails from '@salesforce/apex/AccountController.getAccountDetails';

export default class AccountCard extends NavigationMixin(LightningElement) {
    // @api — public property, settable by parent components
    @api recordId;

    // @track — not needed for objects/arrays in modern LWC (tracked by default)
    isLoaded = false;
    contacts = [];

    // Computed property — getter pattern
    get formattedRevenue() {
        return this.account?.AnnualRevenue
            ? '$' + this.account.AnnualRevenue.toLocaleString()
            : 'N/A';
    }

    handleEdit() {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: this.recordId,
                actionName: 'edit'
            }
        });
    }
}

Lifecycle Hooks

LWC components go through a defined lifecycle. Understanding each hook is critical for placing initialization logic, DOM manipulation, and cleanup code correctly.

constructor()

Called when component is created. Don't access child elements yet — they don't exist. Use for initial property setup.

connectedCallback()

Fires when component is inserted into DOM. Safe to access this.template but child slots may not be rendered yet.

renderedCallback()

Fires after every render. Can fire multiple times — use a boolean guard to prevent repeated initialization. Safe to query child DOM.

disconnectedCallback()

Fires when component is removed from DOM. Clean up event listeners, timers, and subscriptions here to prevent memory leaks.

JavaScript — Lifecycle Hooks Pattern
import { LightningElement } from 'lwc';
import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';

export default class MyComponent extends LightningElement {
    _initialized = false;
    _subscription = null;
    _intervalId = null;

    connectedCallback() {
        // Subscribe to message channel
        this._subscription = subscribe(this.messageContext, MY_CHANNEL, (msg) => {
            this.handleMessage(msg);
        });
        // Start polling
        this._intervalId = setInterval(() => this.refresh(), 30000);
    }

    renderedCallback() {
        // Only run once, even though this fires on every render
        if (this._initialized) return;
        this._initialized = true;
        // Safe to access child elements now
        const canvas = this.template.querySelector('canvas');
        this.initChart(canvas);
    }

    disconnectedCallback() {
        // Always clean up to prevent memory leaks
        unsubscribe(this._subscription);
        clearInterval(this._intervalId);
    }
}

Wire Service

The @wire decorator is the reactive way to fetch data and metadata in LWC. It automatically re-fetches when its reactive properties change and works with both Apex methods and Salesforce data APIs.

JavaScript — Wire Patterns
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import { getPicklistValues, getObjectInfo } from 'lightning/uiObjectInfoApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import getRelatedContacts from '@salesforce/apex/AccountController.getRelatedContacts';

export default class WireDemo extends LightningElement {
    @api recordId;

    // Wire to standard record API — auto-refreshes on recordId change
    @wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, INDUSTRY_FIELD] })
    account;

    // Wire to Apex — cacheable = true required for @wire
    @wire(getRelatedContacts, { accountId: '$recordId' })
    wiredContacts({ error, data }) {
        if (data) {
            this.contacts = data.map(c => ({
                ...c,
                displayName: c.FirstName + ' ' + c.LastName
            }));
        } else if (error) {
            this.error = error.body.message;
        }
    }

    // Wire to picklist values
    @wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT })
    objectInfo;

    @wire(getPicklistValues, { 
        recordTypeId: '$objectInfo.data.defaultRecordTypeId', 
        fieldApiName: INDUSTRY_FIELD 
    })
    industryPicklist;

    // Computed getter using wire data
    get accountName() {
        return getFieldValue(this.account.data, NAME_FIELD);
    }
}

Custom Events & Communication

LWC uses a clear communication model: properties down and events up. Child components dispatch custom events; parent components listen and react.

swap_vert

Communication Pattern: Pass data down via @api properties. Communicate upward via custom CustomEvent. For unrelated components, use Lightning Message Service (LMS).

JavaScript — Child: Dispatching Events
// childForm.js — dispatches event to parent
export default class ChildForm extends LightningElement {

    handleSave() {
        const formData = {
            name: this.template.querySelector('[data-field="name"]').value,
            industry: this.template.querySelector('[data-field="industry"]').value
        };

        // Dispatch event with payload
        this.dispatchEvent(
            new CustomEvent('save', {
                detail: formData,
                bubbles: true,   // bubbles up the DOM tree
                composed: false  // stays within shadow DOM by default
            })
        );
    }

    handleCancel() {
        this.dispatchEvent(new CustomEvent('cancel'));
    }
}
HTML + JavaScript — Parent: Listening to Events
<!-- parentPage.html -->
<template>
    <c-child-form 
        onsave={handleSave} 
        oncancel={handleCancel}>
    </c-child-form>
</template>

// parentPage.js
export default class ParentPage extends LightningElement {

    handleSave(event) {
        const { name, industry } = event.detail;
        // Call Apex or process data
        createAccount({ name, industry })
            .then(() => { this.showSuccess = true; })
            .catch(err => { this.error = err.body.message; });
    }

    handleCancel() {
        this[NavigationMixin.Navigate]({ type: 'standard__objectPage', ... });
    }
}

Lightning Message Service

For communication between components that have no parent-child relationship — across different regions of the page — use Lightning Message Service (LMS).

JavaScript — LMS Publish & Subscribe
// Publisher component
import { publish, MessageContext } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/RecordSelected__c';

export default class Publisher extends LightningElement {
    @wire(MessageContext) messageContext;

    handleRecordSelect(event) {
        publish(this.messageContext, RECORD_SELECTED, {
            recordId: event.detail.id,
            recordName: event.detail.name
        });
    }
}

// Subscriber component
import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/RecordSelected__c';

export default class Subscriber extends LightningElement {
    @wire(MessageContext) messageContext;
    _subscription;

    connectedCallback() {
        this._subscription = subscribe(
            this.messageContext, 
            RECORD_SELECTED, 
            (message) => this.handleMessage(message)
        );
    }

    disconnectedCallback() {
        unsubscribe(this._subscription);
    }

    handleMessage(message) {
        this.selectedId = message.recordId;
    }
}