Salesforce Interview Questions & Answers Book is now available!

Grab Now

Interview questions

LWC Interview Questions and Answers (2026)

Lightning Web Components interview questions with answers that explain the why: decorators, lifecycle hooks, events, LDS caching, wire vs imperative Apex.

By Teja, ex-Salesforce engineer · Updated October 6, 2026 · 15 min read

TL;DR

  • Interviewers rarely want definitions. They want the why: why all fields are reactive but @track still exists, why wired data is read-only, why a cached result goes stale.
  • Know the two client caches (Lightning Data Service records and cacheable Apex results) and how to refresh each one: notifyRecordUpdateAvailable and refreshApex.
  • Write every template without expressions. Put logic in getters and use lwc:if, lwc:elseif and lwc:else.

These are the Lightning Web Components questions that come up in Salesforce developer interviews in 2026, with answers that explain why things work the way they do, because the follow-up is always "why?". They're grouped by level. Start with Fundamentals if you're going for your first developer role, and jump to Scenario-based and Senior / design if you've shipped components before.

Read each question, answer it out loud before you read mine, then compare. Every code sample follows current conventions: lwc:if instead of the legacy directives, and no expressions in templates. Logic lives in JavaScript getters.

Fundamentals

What is Lightning Web Components, and how is it different from Aura?

LWC is Salesforce's component model built on web standards: custom elements, shadow DOM, ES modules and HTML templates. The browser does most of the work natively, so the framework layer on top is thin. Aura was built before those standards existed, so it shipped its own component model, event system and expression language. That made it heavier.

Three practical differences matter in an interview. LWC uses standard JavaScript classes and modules instead of controller and helper files. Data flows down through public properties and up through DOM events, instead of Aura's application and component events. And interoperability is one-way: you can put an LWC inside an Aura component, but you can't put an Aura component inside an LWC.

What the interviewer is really checking: that you see LWC as "the web platform plus a thin layer", not as renamed Aura.

Which files make up an LWC bundle, and which ones are required?

A component folder needs a JavaScript file and a js-meta.xml configuration file. The HTML template is required for any component that renders UI. A service component that only exports functions can skip it. CSS, an SVG icon and a __tests__ folder for Jest tests are optional.

Every file shares the folder name, which starts lowercase and uses camelCase: contactList/contactList.js, contactList.html, contactList.js-meta.xml. In markup you reference it in kebab-case as c-contact-list.

The meta file is where candidates lose marks. isExposed controls whether the component shows up in Lightning App Builder, Experience Builder or Flow Builder, and targets decides where it can be placed.

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>65.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

Explain @api, @track and @wire. When do you still need @track?

@api makes a field or method public. The parent sets the property and the child rerenders when it changes. Inside the child, treat it as read-only.

@wire connects a property or function to a wire adapter, such as getRecord or a cacheable Apex method, and provisions new data whenever a reactive $parameter changes.

@track is the one people get wrong. Every field is already reactive: reassign it and the template updates. For objects and arrays, though, the framework only notices reassignment, not changes made inside them. @track tells it to observe property changes on a plain object and element changes in an array. Even then it doesn't observe mutations inside a Date, Set or Map.

In practice I rarely use @track. Reassigning with a spread is clearer and works everywhere.

// Rerenders: the field now points to a new array
this.items = [...this.items, newItem];

// Doesn't rerender without @track: same array reference
this.items.push(newItem);

What the interviewer is really checking: that you know all fields became reactive, and exactly where that reactivity stops.

Why can't you put a JavaScript expression in an LWC template?

LWC templates bind to properties, such as {contact.Name} or {isEmpty}. You can't write a comparison, a ternary or a function call inside the braces, so {items.length === 0} won't compile.

That's deliberate. Logic in JavaScript is testable with Jest, lives in one place and is easy to review. The template stays declarative. The fix is always a getter:

get isEmpty() {
    return this.contacts.length === 0;
}

get contactCountLabel() {
    return `${this.contacts.length} contacts`;
}
<template lwc:if={isEmpty}>
    <p>No contacts yet.</p>
</template>
<template lwc:else>
    <p>{contactCountLabel}</p>
</template>

The same rule explains why there's no ! operator in lwc:if. Write a hasContacts getter instead of trying to negate a value in markup. I go deeper on this in why LWC templates have no expressions.

What's the difference between lwc:if and the older conditional directives?

lwc:if, lwc:elseif and lwc:else arrived in Spring '23 and replace the legacy if: directives, which Salesforce no longer recommends and may remove in a future release. The new directives give you a real if / else-if / else chain. With the old ones you wrote separate, independent checks, often with an extra getter just to negate a value.

The rules worth remembering: lwc:elseif and lwc:else must sit on the element immediately after an lwc:if or lwc:elseif sibling, and you can't mix the new directives with the legacy ones on the same element. You can use them on a template tag or directly on an element.

<template lwc:if={isLoading}>
    <lightning-spinner alternative-text="Loading"></lightning-spinner>
</template>
<template lwc:elseif={hasError}>
    <p class="slds-text-color_error">{errorMessage}</p>
</template>
<template lwc:else>
    <c-contact-table contacts={contacts}></c-contact-table>
</template>

Walk me through the LWC lifecycle hooks in order.

  1. constructor() runs when the component is created. Call super() first. Public properties aren't set yet and nothing has rendered, so don't touch the DOM or read @api values here.
  2. connectedCallback() runs when the component is inserted into the DOM. Public properties are available. This is the place to start loading data or subscribe to a message channel. It can run more than once if the component is removed and reinserted.
  3. render() is rarely used. It returns which template to render when a component switches between templates.
  4. renderedCallback() runs after every render. Children finish rendering before their parent.
  5. disconnectedCallback() is for cleanup: unsubscribe, clear timers, remove listeners added to window.
  6. errorCallback(error, stack) catches errors thrown in descendants' lifecycle hooks and rendering, which makes the component an error boundary.

Creation flows parent to child. renderedCallback completes child first, then parent.

What the interviewer is really checking: where you load data (connectedCallback), where you don't (constructor), and whether you know renderedCallback runs repeatedly.

How do parent and child components communicate?

Down: the parent sets the child's @api properties in markup. When it needs the child to do something, it calls an @api method on it, for example this.template.querySelector('c-contact-tile').highlight().

Up: the child dispatches a CustomEvent and the parent listens with an on handler in its template. Put the payload in detail, and keep it to primitives or a copy, because a listener can mutate an object you hand it. Event names should be lowercase with no hyphens, and shouldn't start with "on".

// contactTile.js (child)
handleClick() {
    this.dispatchEvent(new CustomEvent('select', {
        detail: { contactId: this.contact.Id }
    }));
}
<c-contact-tile contact={contact} onselect={handleSelect}></c-contact-tile>

Siblings talk through their common parent. Components with no shared parent use Lightning Message Service.

What is Lightning Message Service, and when do you use it instead of events?

Lightning Message Service (LMS) lets components that don't share a parent communicate in Lightning Experience, including Aura components, Visualforce pages and items in the utility bar. Events only travel up the DOM tree, so they can't reach a component in another region of the page.

You define a message channel as LightningMessageChannel metadata, import it with @salesforce/messageChannel/Record_Selected__c, get a context with @wire(MessageContext), then publish on one side and subscribe on the other.

connectedCallback() {
    if (!this.subscription) {
        this.subscription = subscribe(this.messageContext, RECORD_SELECTED, (message) => {
            this.selectedId = message.recordId;
        });
    }
}

disconnectedCallback() {
    unsubscribe(this.subscription);
    this.subscription = null;
}

My rule: parent and child use properties and events, and unrelated components use LMS. The old pubsub module from the sample apps is a legacy workaround, so don't propose it for new code.

Intermediate

When do you use @wire, and when do you call Apex imperatively?

Use @wire to read data that should follow reactive parameters. The Apex method must be @AuraEnabled(cacheable=true), results are cached on the client, and the wire service decides when to call. Expect it to provision once with no data before the real result arrives.

Call Apex imperatively when a user action triggers the call, when the method does DML (a cacheable method can't), or when you need to control the timing and order of calls. It returns a promise.

import getContacts from '@salesforce/apex/ContactController.getContacts';
import saveContact from '@salesforce/apex/ContactController.saveContact';

@api recordId;

@wire(getContacts, { accountId: '$recordId' })
contacts;

async handleSave() {
    try {
        await saveContact({ contact: this.draft });
    } catch (error) {
        this.error = error;
    }
}

What the interviewer is really checking: that you know cacheable=true rules out DML, so writes always go imperative.

Why is data from @wire read-only, and how do you change it?

Wired data comes from a cache that other components share. If one component could change it in place, every component reading the same data would see a value that never went to the server. So LWC gives you read-only objects, and trying to assign to their properties fails.

When you need to add UI state, such as a selected flag, copy the data first:

@wire(getContacts, { accountId: '$recordId' })
wiredContacts({ data, error }) {
    if (data) {
        this.contacts = data.map((contact) => ({ ...contact, isSelected: false }));
        this.error = undefined;
    } else if (error) {
        this.error = error;
        this.contacts = [];
    }
}

The spread makes a shallow copy, which is enough for flat records. The same rule applies to objects a parent passes down through @api: the child should copy them, not mutate them.

How does Lightning Data Service caching work, and when does it go stale?

Lightning Data Service (LDS) keeps a client-side cache of records that every component shares. Ten components showing the same Account cost one server request. When you save through LDS, using lightning-record-form, updateRecord, createRecord or deleteRecord, it updates the cache and every component wired to that record with getRecord receives the new values. LDS also respects the running user's CRUD, field-level security and sharing.

The cache goes stale when data changes outside LDS: an imperative Apex method that updates records, a trigger that changes a related record, or an integration. LDS has no way to know. After your Apex save, tell it:

import { notifyRecordUpdateAvailable } from 'lightning/uiRecordApi';

await closeCasesInApex({ caseIds: this.selectedIds });
await notifyRecordUpdateAvailable(this.selectedIds.map((recordId) => ({ recordId })));

notifyRecordUpdateAvailable replaced the older getRecordNotifyChange. It refreshes LDS records only. Results from cacheable Apex methods live in a separate cache, and refreshApex is what refreshes those.

What the interviewer is really checking: that you know there are two caches and which tool invalidates each one.

How do you refresh a wired Apex result after saving data?

Call refreshApex and pass it the exact value the wire provisioned, not a copy and not the destructured data. This is the most common bug: someone destructures { data, error } in the wired function, stores a mapped copy, passes the copy to refreshApex and nothing happens.

import { refreshApex } from '@salesforce/apex';

wiredResult;

@wire(getContacts, { accountId: '$recordId' })
wiredContacts(result) {
    this.wiredResult = result;
    const { data, error } = result;
    if (data) {
        this.contacts = data;
    } else if (error) {
        this.error = error;
    }
}

async handleSave() {
    await saveContact({ contact: this.draft });
    await refreshApex(this.wiredResult);
}

refreshApex goes back to the server, provisions the fresh result through the same wire and returns a promise, so you can show a spinner until it resolves.

How does the shadow DOM affect styling and querying elements?

Each component's markup and CSS are encapsulated. Your styles apply only to your own template: a parent's CSS doesn't leak into a child, and a child's doesn't leak out. this.template.querySelector() only finds elements in your own template, not inside child components, and you shouldn't use document.querySelector to reach into components at all.

To change how a child looks, use the levers it gives you: @api properties such as a variant, or styling hooks, which are CSS custom properties that base components expose.

You can opt a component out of shadow DOM with light DOM: set static renderMode = 'light' in the class and lwc:render-mode="light" on the root template, then query with this.querySelector(). You lose encapsulation, so use it deliberately, for example when a site's global stylesheet or a third-party library needs to reach the markup.

What the interviewer is really checking: that you won't fight encapsulation with global CSS hacks.

How do you expose a component on a record page and get the record Id?

Set isExposed to true, add the lightning__RecordPage target and declare @api recordId. When the component sits directly on a record page, the page sets recordId (and objectApiName, if you declare it). Use it as a reactive parameter, $recordId, in your wires.

Add targetConfigs to give admins settings in Lightning App Builder:

<isExposed>true</isExposed>
<targets>
    <target>lightning__RecordPage</target>
</targets>
<targetConfigs>
    <targetConfig targets="lightning__RecordPage">
        <property name="maxRows" type="Integer" default="10" label="Rows to show"/>
        <objects>
            <object>Account</object>
        </objects>
    </targetConfig>
</targetConfigs>

The trap: only the component placed on the page receives recordId automatically. A component nested inside it gets nothing unless the parent passes recordId down as a property.

How do you handle errors from Apex calls in LWC?

A wired call gives you error in the provisioned value. An imperative call rejects, so you catch it. The error's shape varies: an Apex exception usually arrives with a body.message, while UI API errors can carry field-level and page-level errors in a different structure. Normalise them with one small helper, such as the reduceErrors function from the lwc-recipes sample app, instead of reading error.body.message everywhere.

On the Apex side, throw an AuraHandledException with a message you're happy for the user to see. That way you decide what reaches the browser and don't leak internals.

import { ShowToastEvent } from 'lightning/platformShowToastEvent';

async handleSave() {
    try {
        await saveContact({ contact: this.draft });
    } catch (error) {
        this.dispatchEvent(new ShowToastEvent({
            title: 'Could not save the contact',
            message: reduceErrors(error).join(', '),
            variant: 'error'
        }));
    }
}

Scenario-based

Your page freezes after you add code to renderedCallback. What happened?

You've almost certainly built a render loop. renderedCallback runs after every render. If it changes a field the template uses, say by assigning a new array, the component renders again, which calls renderedCallback again, and so on.

Two fixes. Move data preparation out of renderedCallback and into connectedCallback, the wire handler or a getter. And guard one-time DOM work, such as initialising a chart library, with a flag:

chartInitialised = false;

renderedCallback() {
    if (this.chartInitialised) {
        return;
    }
    this.chartInitialised = true;
    this.initialiseChart();
}

The flag isn't used in the template, so changing it doesn't trigger another render.

What the interviewer is really checking: that you know renderedCallback isn't a one-time hook.

A parent pushes an item into an array it passes to a child, but the child doesn't update. Why?

push mutates the existing array. Unless the parent's field is decorated with @track, reassignment is the only change the framework sees, so the parent doesn't rerender and the child keeps receiving the same reference.

// Parent: the child updates because it receives a new array
addContact(contact) {
    this.contacts = [...this.contacts, contact];
}

Don't try to fix it in the child by mutating the @api array, either. Objects a parent passes down are read-only to the child. If the child needs its own version, it copies the data, and if the data must change, the child fires an event and lets the parent, which owns the data, change it.

A wired list shows stale data after another component saves a record. How do you fix it?

First work out which cache is stale.

  • The list comes from a cacheable Apex method. Nothing told it the data changed. If the save happens in the same component, call refreshApex on the stored wire result after the save. If it happens in a different component, publish a Lightning Message Service message after the save, and have the list component call refreshApex when it receives it.
  • The save went through Apex, and standard record components on the page are stale. Call notifyRecordUpdateAvailable with the changed record Ids so LDS refetches them.
  • Both components use LDS for the same record, with getRecord and updateRecord. The shared cache keeps them in sync with no extra code.

A candidate who says "I'd just remove cacheable=true" fails this one. It breaks @wire and throws away client caching for every user.

How would you build a search box that queries Apex as the user types?

Debounce the input so you don't call Apex on every keystroke, then let a reactive wire parameter do the call:

import { LightningElement, wire } from 'lwc';
import searchContacts from '@salesforce/apex/ContactSearchController.searchContacts';

const DELAY_MS = 300;

export default class ContactSearch extends LightningElement {
    searchKey = '';
    delayTimeout;

    @wire(searchContacts, { searchKey: '$searchKey' })
    contacts;

    handleKeyChange(event) {
        clearTimeout(this.delayTimeout);
        const value = event.target.value;
        // eslint-disable-next-line @lwc/lwc/no-async-operation
        this.delayTimeout = setTimeout(() => {
            this.searchKey = value;
        }, DELAY_MS);
    }

    disconnectedCallback() {
        clearTimeout(this.delayTimeout);
    }
}

Read event.target.value before the timeout, not inside it. Because the Apex method is cacheable, repeating a search the user already ran is served from the client cache. On the server, use a bind variable and WITH USER_MODE in the query, and return early when the search term is too short to be useful.

An event from a grandchild never reaches your handler. What's wrong?

By default a CustomEvent has bubbles: false and composed: false. Only listeners on the element itself hear it, which in practice means the component whose template contains the child. The grandparent never sees it.

You have three options:

  1. The middle component handles the event and dispatches its own. This is usually the best answer, because each component's events stay part of its own documented API.
  2. Set bubbles: true, composed: true. The event crosses every shadow boundary up to the document. It works, but now it's part of the public API of every component it passes through, and names can collide.
  3. Use Lightning Message Service if the two components are far apart.

Also check the name. An event named with uppercase letters or hyphens can't be handled with an on attribute in markup.

What the interviewer is really checking: that you understand composed and its cost, not just how to make the event arrive.

How do you use an LWC inside a screen flow and pass values back to the flow?

Add the lightning__FlowScreen target and declare the properties in targetConfig, with role="inputOnly" or role="outputOnly" where a property only goes one way. Back them with @api properties.

When a value changes, dispatch a FlowAttributeChangeEvent so the flow receives it. To validate before the user can move on, expose an @api validate() method.

import { LightningElement, api } from 'lwc';
import { FlowAttributeChangeEvent } from 'lightning/flowSupport';

export default class PriorityPicker extends LightningElement {
    @api priority;

    handleChange(event) {
        this.dispatchEvent(new FlowAttributeChangeEvent('priority', event.detail.value));
    }

    @api
    validate() {
        if (this.priority) {
            return { isValid: true };
        }
        return { isValid: false, errorMessage: 'Choose a priority to continue.' };
    }
}

To move the flow forward from your own button, dispatch FlowNavigationNextEvent, after checking that NEXT is in the availableActions the flow passes in.

Senior / design

How do you unit test a Lightning web component?

With Jest, using @salesforce/sfdx-lwc-jest. Tests run locally in Node with no org. You create the component with createElement, attach it to document.body, and assert on its shadowRoot. Apex imports are mocked with jest.mock, and wire adapters are mocked with test adapters that let you emit() data or errors.

Rendering is asynchronous, so after you change a property, wait for a microtask before asserting:

import { createElement } from 'lwc';
import ContactList from 'c/contactList';

describe('c-contact-list', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('shows the empty state when there are no contacts', async () => {
        const element = createElement('c-contact-list', { is: ContactList });
        element.contacts = [];
        document.body.appendChild(element);

        await Promise.resolve();

        expect(element.shadowRoot.querySelector('.empty-state')).not.toBeNull();
    });
});

Getters make this easy, which is another reason to keep logic out of templates.

Where does security live in an LWC and Apex solution?

On the server. The browser is never the security boundary. Any user with access to your Apex class can call its @AuraEnabled methods directly, with any arguments, without your component ever loading. Hiding a button in the UI protects nothing.

So the Apex does the work: with sharing for record access, WITH USER_MODE in queries and user-mode DML for object and field permissions, validation of every input, and returning only the fields the component needs. Prefer Lightning Data Service where it fits, because it enforces CRUD, FLS and sharing for you.

On the client, Lightning Web Security, the successor to Lightning Locker, isolates components from different namespaces from each other. That protects components from each other. It doesn't protect your data from your users.

What the interviewer is really checking: that you'd never rely on the UI to restrict data.

How would you design a complex page built from many components?

I start with one container component that owns the data and the state: it wires or calls Apex, handles errors and loading, and decides what to show. Its children are presentational: data comes in through @api, intent goes out through events, and they never call the server. Presentational components are easy to reuse and easy to test.

Shared logic, such as formatting or error reduction, goes into service modules: components that only export JavaScript functions. Components in different regions of the page coordinate through Lightning Message Service, not through composed events.

For server calls, prefer LDS for single records, and when you need Apex, make one call that returns what the screen needs instead of five small ones. Keep getters cheap, because they run on every render. For heavy, rarely used parts of the page, render them only when needed with lwc:if, or load them on demand with dynamic components (lwc:component with lwc:is), which need Lightning Web Security enabled and the lightning__dynamicComponent capability in the meta file.

How to practise these LWC questions out loud

Reading answers makes you feel ready. Saying them is what makes you ready. This is how I'd use this page:

  1. Answer before you read. Cover my answer, say yours out loud, then compare. Note which questions made you hesitate.
  2. Answer in three parts: a one-sentence definition, the why, then a real example from something you built. Two minutes, no more.
  3. Build the scenario questions. Create the render loop, the stale refreshApex call and the event that doesn't reach the grandparent in a Developer Edition org. Breaking something once teaches you more than any written answer.
  4. Write code without autocomplete. Interview editors rarely give you IntelliSense. Write a wired component and its getters on a blank page.
  5. Record yourself. You'll hear the filler words and the answers that wander.

If you'd rather learn this by watching it built, the Lightning Web Components course builds each of these patterns live, line by line, in a real org, and the Interview Questions course works through common questions the same way. The Lifetime plan also includes the 700+ page Interview Questions Book.

Want 700+ pages of this?

The Salesforce Interview Questions & Answers Book goes far deeper than this page — it's included with the Lifetime plan, alongside the Real-Time Projects series.

Frequently asked questions

How much of a Salesforce developer interview is about LWC?

For front-end or full-stack Salesforce roles, expect LWC to take a large share of at least one technical round, usually alongside Apex. Back-end and integration roles still ask the basics: decorators, events and calling Apex.

Do LWC interviews include live coding?

Often, yes. A common format is building a small component in a shared editor or a Developer Edition org, or reviewing a buggy component and explaining what's wrong. Practise writing components from a blank file without autocomplete.

How much JavaScript do I need for an LWC interview?

Modern JavaScript fundamentals: classes, modules, arrow functions, destructuring, spread, promises and async/await, plus how DOM events bubble. Most failed LWC answers are really JavaScript gaps.

Should I learn Aura before an LWC interview?

Not for new development, which is LWC. If the role maintains an older org, expect a question on how LWC and Aura interoperate, such as putting an LWC inside an Aura component.

What should I talk about from my own projects?

Pick one component you built and be ready to explain its data flow, how it called Apex or Lightning Data Service, how it handled errors and how you tested it. Specific trade-offs beat a list of features.

Keep going

Keep me in the loop!

Email me when a new episode is published (promise, I will not spam you)

You're signing up to receive emails from Salesforce Casts