Struggling with LWC? Here’s the Only Guide You Need

Published On: March 29, 2026

Struggling with LWC? This guide simplifies everything—from basics to advanced concepts—with real-world examples, helping you master Lightning Web Components and confidently crack Salesforce interviews.

Salesforce LWC

Lightning Web Components — Complete Guide

Full component anatomy, lifecycle hooks, decorators, communication patterns, wire service, best practices, and scenario-based interview questions with complete copy-ready code.

Component Basics
Lifecycle Hooks
Decorators
Wire Service
Communication
Interview Q&A
Scenarios
Basics & Anatomy
Lifecycle Hooks
Decorators
Wire & Apex
Communication
Best Practices
Interview Q&A
Scenarios
What is LWC?

Lightning Web Components (LWC) is Salesforce’s modern UI framework built on web standards — Custom Elements, Shadow DOM, and ES Modules. Every LWC component is a folder containing an HTML template, a JavaScript controller, and optionally a CSS file and metadata XML.

HTML
Template file
Defines the UI using HTML with special LWC directives. Always wrapped in a single <template> tag.
JavaScript
Controller class
ES6+ class extending LightningElement. Contains properties, methods, lifecycle hooks, and event handlers.
CSS
Scoped styles
Styles are scoped to the component via Shadow DOM. External styles cannot bleed in; internal styles cannot bleed out.
XML
Metadata config
Controls where the component can be used — App pages, Record pages, Home pages, or exposed as a tab.
myComponent/myComponent.html — template anatomy
<!-- myComponent.html -->
<template>

    <!-- Conditional rendering -->
    <template lwc:if={isLoaded}>
        <p>Hello, {greeting}</p>
    </template>
    <template lwc:else>
        <p>Loading...</p>
    </template>

    <!-- Iteration -->
    <template for:each={items} for:item="item">
        <li key={item.id}>{item.name}</li>
    </template>

    <!-- Event binding -->
    <lightning-button
        label="Save"
        onclick={handleSave}
    ></lightning-button>

    <!-- Child component -->
    <c-child-component
        record-id={recordId}
        onchildevent={handleChildEvent}
    ></c-child-component>

</template>
myComponent/myComponent.js — controller skeleton
import { LightningElement, api, track, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class MyComponent extends LightningElement {

    // Public property — parent can pass value in
    @api recordId;
    @api greeting = 'World';

    // Reactive private property — re-renders on change
    @track items = [];
    isLoaded = false;
    error;

    // Wire — auto-calls Apex when recordId changes
    @wire(getAccounts, { recordId: '$recordId' })
    wiredAccounts({ error, data }) {
        if (data) {
            this.items = data;
            this.isLoaded = true;
        } else if (error) {
            this.error = error;
        }
    }

    // Lifecycle hook
    connectedCallback() {
        console.log('Component mounted to DOM');
    }

    // Event handler
    handleSave() {
        this.dispatchEvent(new ShowToastEvent({
            title: 'Success',
            message: 'Record saved!',
            variant: 'success'
        }));
    }

    // Getter — computed property
    get hasItems() {
        return this.items && this.items.length > 0;
    }
}
myComponent/myComponent.js-meta.xml — metadata
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <property name="greeting" type="String" label="Greeting Text"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
LWC lifecycle hooks

Lifecycle hooks let you run code at specific points in a component’s life — when it’s created, inserted into the DOM, rendered, or removed. Understanding the exact order is a critical interview topic.

Lifecycle execution order
constructor()
01
connected Callback()
02
render()
03
rendered Callback()
04
property change → re-render
05
disconnected Callback()
06
All lifecycle hooks — complete reference with use cases
import { LightningElement, api } from 'lwc';

export default class LifecycleDemo extends LightningElement {

    /**
     * constructor()
     * - First hook called when component instance is created
     * - Called BEFORE the component is inserted into DOM
     * - Never access this.template here (DOM not ready)
     * - Always call super() first
     * - Use: initialize primitive properties
     */
    constructor() {
        super();
        // OK: set primitive defaults
        this.title = 'Initial value';
        // NOT OK: this.template.querySelector(...) — DOM not ready yet
    }

    /**
     * connectedCallback()
     * - Called when component is INSERTED into the DOM
     * - this.template is accessible
     * - Use: fetch data, add event listeners, subscribe to message channels
     * - Fires for every re-insertion (navigation, conditional rendering)
     */
    connectedCallback() {
        this.loadInitialData();
        window.addEventListener('resize', this.handleResize.bind(this));
    }

    /**
     * render()
     * - Called to determine which template to render
     * - Must return a template reference (imported HTML file)
     * - Use: conditional template switching (e.g. edit vs read mode)
     * - RARE — most components don't need this hook
     */
    render() {
        return this.isEditMode ? editTemplate : viewTemplate;
    }

    /**
     * renderedCallback()
     * - Called AFTER every render (first render + re-renders)
     * - DOM is fully painted — safe to use this.template.querySelector()
     * - Use: third-party JS lib init, DOM measurements, canvas operations
     * - IMPORTANT: guard with a flag to avoid infinite loops
     */
    renderedCallback() {
        if (this.hasRendered) return; // prevent re-run on every re-render
        this.hasRendered = true;
        // Safe to access DOM now
        const input = this.template.querySelector('input');
        if (input) input.focus();
    }

    /**
     * disconnectedCallback()
     * - Called when component is REMOVED from the DOM
     * - Use: clean up event listeners, unsubscribe from channels
     * - Prevents memory leaks
     */
    disconnectedCallback() {
        window.removeEventListener('resize', this.handleResize);
        // unsubscribe from MessageChannel if subscribed
    }

    /**
     * errorCallback(error, stack)
     * - Called when a child component throws an error
     * - ONLY works on PARENT components — acts as an error boundary
     * - Use: graceful error UI, logging
     */
    errorCallback(error, stack) {
        console.error('Child error:', error);
        console.error('Stack:', stack);
        this.errorMessage = error.message;
    }
}
⚠
Critical interview trap: renderedCallback() fires on EVERY render, not just the first one. Always guard with a boolean flag like hasRendered to prevent infinite re-render loops caused by setting reactive properties inside it.
LWC decorators — @api, @track, @wire

Decorators are the most commonly tested LWC topic in interviews. Understanding the exact difference between them — especially @api vs @track — is essential.

@api
Public property
Exposes a property or method to a parent component. Creates a one-way data flow: parent → child. Always reactive — re-renders when parent changes the value.
@track
Deep reactive property
Makes nested object/array mutations reactive. In modern LWC (API v39+), all properties are reactive by default. @track is needed only for deep object mutations.
@wire
Reactive data binding
Connects component to a Salesforce data source (Apex method or wire adapter). Re-runs automatically when reactive parameters (prefixed with $) change.
@api — public properties and methods
import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {

    // Simple @api property — parent passes value via HTML attribute
    @api title = 'Default Title';
    @api recordId;
    @api isReadOnly = false;

    // @api with getter/setter — intercept changes from parent
    _items = [];
    @api
    get items() {
        return this._items;
    }
    set items(value) {
        // Transform data as it arrives from parent
        this._items = value ? [...value].sort((a, b) => a.name.localeCompare(b.name)) : [];
    }

    // @api method — parent can call this directly via template ref
    @api
    resetForm() {
        this.template.querySelectorAll('lightning-input')
            .forEach(input => input.reset());
    }

    // WRONG: Never mutate @api props inside the child — read-only from child
    // this.recordId = 'newId'; // ← this throws an error in LWC
}

// Parent HTML usage:
<c-child-component
    title="My Title"
    record-id={currentRecordId}
    items={accountList}
></c-child-component>

// Parent JS — calling @api method via template ref:
// this.template.querySelector('c-child-component').resetForm();
@track — when you actually need it vs when you don’t
import { LightningElement, track } from 'lwc';

export default class TrackDemo extends LightningElement {

    // Since API v39+, primitive reassignment is reactive WITHOUT @track
    count = 0;           // reactive — this.count++ triggers re-render
    name = '';            // reactive — this.name = 'x' triggers re-render
    isOpen = false;       // reactive — this.isOpen = true triggers re-render

    // Object REASSIGNMENT is reactive WITHOUT @track
    user = { name: 'Alice', age: 30 };
    replaceUser() {
        // Reassigning the whole object — reactive, no @track needed
        this.user = { name: 'Bob', age: 25 };
    }

    // Deep MUTATION of nested object/array — needs @track
    @track config = {
        filters: { status: 'Active', type: 'Customer' },
        pagination: { page: 1, pageSize: 10 }
    };

    updateFilter() {
        // Mutating a nested property — ONLY reactive because of @track
        this.config.filters.status = 'Inactive'; // re-renders
        this.config.pagination.page = 2;         // re-renders
    }

    // @track on arrays — needed for push/splice/sort mutations
    @track rows = [];
    addRow() {
        // .push() mutates in place — needs @track to be reactive
        this.rows.push({ id: Date.now(), value: 'New' });
    }
}
Wire service & Apex integration

The @wire decorator is the primary way LWC components talk to Salesforce data. Wire adapters auto-refresh when reactive parameters change. For imperative DML operations, use Apex methods directly with async/await.

AccountController.cls — Apex methods for LWC
public with sharing class AccountController {

    /**
     * @AuraEnabled(cacheable=true) — required for @wire
     * cacheable=true: result is cached, read-only SOQL only
     * cacheable=false: for DML operations, used imperatively
     */
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts(String searchKey) {
        String key = '%' + searchKey + '%';
        return [
            SELECT Id, Name, Industry, AnnualRevenue, Phone
            FROM Account
            WHERE Name LIKE :key
            ORDER BY Name
            LIMIT 50
        ];
    }

    // NOT cacheable — used imperatively for DML
    @AuraEnabled
    public static Account saveAccount(Account accountRecord) {
        try {
            upsert accountRecord;
            return accountRecord;
        } catch (Exception e) {
            throw new AuraHandledException(e.getMessage());
        }
    }

    @AuraEnabled(cacheable=true)
    public static Account getAccountById(Id recordId) {
        return [
            SELECT Id, Name, Industry, Phone, BillingCity
            FROM Account
            WHERE Id = :recordId
            LIMIT 1
        ];
    }
}
accountList.js — wire vs imperative Apex, complete patterns
import { LightningElement, api, wire, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
import getAccountById from '@salesforce/apex/AccountController.getAccountById';
import saveAccount from '@salesforce/apex/AccountController.saveAccount';

export default class AccountList extends LightningElement {

    @api recordId;
    searchKey = '';
    isLoading = false;
    wiredAccountsResult; // store wire result for refreshApex

    // ─── PATTERN 1: @wire with function handler ──────────────────
    // Best when you need to handle data AND error separately
    accounts = [];
    error;

    @wire(getAccounts, { searchKey: '$searchKey' })
    wiredAccounts(result) {
        this.wiredAccountsResult = result; // store for refreshApex
        const { data, error } = result;
        if (data) {
            this.accounts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.accounts = [];
        }
    }

    // ─── PATTERN 2: @wire directly to property ───────────────────
    // Simpler — good when you just display data
    @wire(getAccountById, { recordId: '$recordId' })
    account;
    // Template: {account.data.Name}, {account.error}

    // ─── PATTERN 3: Imperative Apex call (async/await) ───────────
    // Use for: DML, user-triggered actions, conditional calling
    async handleSave(event) {
        const accountToSave = {
            Id: this.recordId,
            Name: event.detail.name,
            Phone: event.detail.phone
        };

        this.isLoading = true;
        try {
            await saveAccount({ accountRecord: accountToSave });
            this.dispatchEvent(new ShowToastEvent({
                title: 'Success', message: 'Account saved', variant: 'success'
            }));
            // Refresh @wire cache after DML
            await refreshApex(this.wiredAccountsResult);
        } catch (error) {
            this.dispatchEvent(new ShowToastEvent({
                title: 'Error',
                message: error.body?.message || 'Unknown error',
                variant: 'error'
            }));
        } finally {
            this.isLoading = false;
        }
    }

    handleSearchChange(event) {
        this.searchKey = event.target.value; // $ prefix makes @wire re-fire
    }
}
i
Key rule: Use cacheable=true only for read-only SOQL queries — this enables @wire. For any Apex method that does DML (insert/update/delete), use cacheable=false and call it imperatively. Always wrap imperative calls in try/catch/finally.
Component communication patterns

LWC has four communication patterns depending on the relationship between components. Choosing the right pattern is a top senior interview question.

PatternDirectionRelationshipMechanism
@api propertyParent → ChildDirect parent-childHTML attribute binding
Custom EventChild → ParentDirect parent-childdispatchEvent + event listener
LMSAny → AnyUnrelated componentsLightning Message Service
PubSubAny → AnySame page, unrelatedCustom pubsub utility (legacy)
Child → Parent: Custom Events — complete pattern
// ─── childComponent.js ──────────────────────────────────────
import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {

    @api label = 'Submit';

    handleClick() {
        // Create custom event with detail payload
        const evt = new CustomEvent('recordselect', {
            detail: {
                recordId: '001xx000003GYkl',
                recordName: 'Acme Corp'
            },
            bubbles: true,    // event bubbles up through DOM tree
            composed: false   // false = stays inside shadow boundary
        });
        this.dispatchEvent(evt);
    }
}

// childComponent.html
<template>
    <lightning-button label={label} onclick={handleClick}></lightning-button>
</template>

// ─── parentComponent.js ──────────────────────────────────────
import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {

    selectedRecord = {};

    handleRecordSelect(event) {
        // Access the detail payload from child event
        const { recordId, recordName } = event.detail;
        this.selectedRecord = { recordId, recordName };
        console.log('Selected:', recordName);
    }
}

// parentComponent.html
<template>
    <!-- Event name becomes: on + eventname = onrecordselect -->
    <c-child-component
        label="Pick Account"
        onrecordselect={handleRecordSelect}
    ></c-child-component>
    <p>Selected: {selectedRecord.recordName}</p>
</template>
Lightning Message Service (LMS) — unrelated component communication
// ─── accountMessageChannel.messageChannel-meta.xml ──────────
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>AccountMessageChannel</masterLabel>
    <isExposed>true</isExposed>
    <fields>
        <fieldName>recordId</fieldName>
        <description>The selected record Id</description>
    </fields>
    <fields>
        <fieldName>action</fieldName>
        <description>Action type: select, update, delete</description>
    </fields>
</LightningMessageChannel>

// ─── Publisher component JS ──────────────────────────────────
import { LightningElement, wire } from 'lwc';
import { MessageContext, publish } from 'lightning/messageService';
import ACCOUNT_MC from '@salesforce/messageChannel/AccountMessageChannel__c';

export default class PublisherComponent extends LightningElement {

    @wire(MessageContext) messageContext;

    handleRowSelect(event) {
        const payload = {
            recordId: event.detail.row.Id,
            action: 'select'
        };
        publish(this.messageContext, ACCOUNT_MC, payload);
    }
}

// ─── Subscriber component JS ─────────────────────────────────
import { LightningElement, wire } from 'lwc';
import { MessageContext, subscribe, unsubscribe } from 'lightning/messageService';
import ACCOUNT_MC from '@salesforce/messageChannel/AccountMessageChannel__c';

export default class SubscriberComponent extends LightningElement {

    @wire(MessageContext) messageContext;
    subscription = null;
    selectedId;

    connectedCallback() {
        this.subscription = subscribe(
            this.messageContext,
            ACCOUNT_MC,
            (message) => this.handleMessage(message)
        );
    }

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

    disconnectedCallback() {
        unsubscribe(this.subscription);
        this.subscription = null;
    }
}
LWC best practices & code standards

These rules separate good LWC developers from great ones. Each rule maps to a real performance issue or common bug pattern.

1
Never mutate @api properties inside the child component
@api properties flow one-way: parent → child. Mutating them in the child throws a runtime error. Use a private copy with a getter/setter, or fire a custom event to ask the parent to update.
Never do this
@api items = [];

addItem() {
  // Throws: Cannot set property
  this.items.push({id:1});
}
Do this instead
@api items = [];

addItem() {
  // Fire event — parent handles
  this.dispatchEvent(
    new CustomEvent('additem', {
      detail: { id: 1 }
    })
  );
}
2
Guard renderedCallback with a boolean flag
renderedCallback fires on every re-render. If you set a reactive property inside it, you trigger another render → infinite loop. Always use a hasRendered guard for one-time DOM operations.
3
Always unsubscribe from LMS in disconnectedCallback
Subscriptions are not automatically cleaned up when a component is removed. Failure to unsubscribe causes memory leaks and ghost event handlers that fire after the component is gone.
4
Use lwc:if / lwc:else instead of the deprecated if:true
As of API v58+, if:true and if:false are deprecated. Use lwc:if, lwc:elseif, and lwc:else directives. These also improve tree-shaking.
5
Always use with sharing in Apex controllers
LWC Apex controllers should always use with sharing to enforce record-level security. Never use without sharing unless you have a documented security exception and compensating controls.
6
Use getters for computed/derived values instead of @track
Instead of maintaining a separate tracked property that you update manually, use a JavaScript getter. Getters recompute automatically when their source properties change and keep the template clean.
Getters — the clean pattern for derived state
export default class GetterDemo extends LightningElement {

    accounts = [];
    searchKey = '';
    sortField = 'Name';

    // Getter — no @track needed, auto-recomputes when dependencies change
    get filteredAccounts() {
        return this.accounts
            .filter(a => a.Name.toLowerCase().includes(this.searchKey.toLowerCase()))
            .sort((a, b) => a[this.sortField].localeCompare(b[this.sortField]));
    }

    get hasAccounts() {
        return this.filteredAccounts.length > 0;
    }

    get accountCount() {
        return `Showing ${this.filteredAccounts.length} of ${this.accounts.length} accounts`;
    }

    get isButtonDisabled() {
        return !this.searchKey || this.isLoading;
    }
}

// Template usage — clean, no manual state management:
// <template lwc:if={hasAccounts}>
//   <p>{accountCount}</p>
//   <template for:each={filteredAccounts} for:item="acc">
//     <p key={acc.Id}>{acc.Name}</p>
//   </template>
// </template>
Interview questions & answers

Tap any question to expand the full answer. Covers easy to senior-level LWC topics.

Scenario-based questions with complete code

Real-world LWC scenarios from Salesforce developer interviews. Each includes the full HTML, JS, Apex, and test class.

Best of Luck


Trusted by 2000+ learners to crack interviews at TCS, Infosys, Wipro, EY, and more.

Want more Real Salesforce Interview Q&As?

For All Job Seekers – 500+ Questions from Top Tech Companies → https://trailheadtitanshub.com/500-real-interview-questions-answers-from-top-tech-companies-ey-infosys-tcs-dell-salesforce-more/

Mega Interview Packs:

 Career Boosters:

Visit us On→ www.trailheadtitanshub.com

TrailheadTitans

At TrailheadTitans.com, we are dedicated to paving the way for both freshers and experienced professionals in the dynamic world of Salesforce. Founded by Abhishek Kumar Singh, a seasoned professional with a rich background in various IT companies, our platform aims to be the go-to destination for job seekers seeking the latest opportunities and valuable resources.

Leave a Comment

Item added to cart.
0 items - ₹0.00