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.

Related Post

Interview Q & A

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

By TrailheadTitans
|
March 29, 2026
Interview Q & A

Everything You Need to Know About Apex Triggers in Salesforce

By TrailheadTitans
|
March 21, 2026
Interview Q & A

If You Know These 20 LWC Questions, Your Next Salesforce Interview Will Feel Easy

By TrailheadTitans
|
March 14, 2026
Interview Q & A

⚡Understanding Governor Limits in Salesforce (Complete Guide for Developers)

By TrailheadTitans
|
March 7, 2026

Leave a Comment