Lightning Web Components (LWC) has become the dominant UI framework in the Salesforce ecosystem. Since Salesforce introduced LWC in 2019, it has rapidly replaced Aura Components as the recommended approach for building fast, modern UIs on the platform. Whether you are interviewing for a Salesforce Developer, Platform Developer II, or Senior Salesforce Engineer role, you can expect several LWC-specific questions in your technical round.
This guide covers the 25 most frequently asked LWC interview questions with detailed, precise answers that go beyond surface-level definitions. Each answer is written to satisfy a senior interviewer — not just to check the checkbox. We cover decorators, lifecycle hooks, parent-child communication, Shadow DOM, @wire, imperative Apex calls, SLDS, navigation mixins, and Jest testing. Work through every question, run the code examples in a scratch org, and you will walk into your interview with genuine confidence.
Table of Contents
- What is LWC and how does it differ from Aura?
- What are the three decorators in LWC?
- Difference between @track and @api?
- LWC component lifecycle hooks in order
- How do you pass data from parent to child?
- How do you pass data from child to parent?
- What is Shadow DOM and how does it affect styling?
- How does the @wire decorator work?
- CustomEvent vs standard events?
- How do you call Apex imperatively from LWC?
- Wire vs imperative Apex calls?
- How do you handle errors from @wire or Apex?
- What is pubsub and when do you use it?
- How do you access the DOM in LWC?
- What are slots in LWC?
- How do you conditionally render elements?
- lwc:if vs the old if:true?
- How do you render a list?
- What is SLDS and how do you use it in LWC?
- What are LWC navigation mixins?
- How do you test LWC components?
- What is @salesforce/schema in LWC?
- What are LWC base components?
- How does LWC handle reactivity?
- Difference between LWC and React?
Section 1: LWC Fundamentals (Q1–Q8)
Lightning Web Components is a modern JavaScript framework built by Salesforce using native web standards — ES6+ classes, Web Components, Shadow DOM, and Custom Elements. It was introduced in Spring '19 as the next-generation component model to replace Aura (also known as Lightning Components).
Key differences between LWC and Aura:
- Performance: LWC is significantly faster. It compiles to standard browser APIs with no heavy framework abstraction layer. Aura relied on a JavaScript framework that added considerable runtime overhead.
- Standard-based: LWC uses native browser features (Custom Elements, Shadow DOM, ES Modules). Aura used proprietary Salesforce abstractions that differ from any web standard.
- Syntax: LWC uses a standard JavaScript class with decorators. Aura used a mix of XML markup and a custom controller/helper/model split across multiple files.
- Interoperability: Aura components can contain LWC components, but not the other way around. LWC components cannot directly call Aura component methods.
- Testing: LWC can be tested with Jest (a standard JS testing framework). Aura required Salesforce's proprietary testing utilities.
- Events: LWC uses standard DOM events and CustomEvent API. Aura had its own component event and application event system.
Salesforce's recommendation as of 2026 is to use LWC for all new development. Aura is in maintenance mode.
LWC provides three decorators imported from the lwc module: @api, @track, and @wire.
- @api — Marks a property or method as public. Parent components can set the property or call the method. Use it for any data or behavior you want to expose outside the component.
- @track — In LWC, all reactive properties are tracked by default as of Spring '20.
@trackis now only needed when you want the framework to deeply observe changes inside a nested object or array (mutations to nested properties). For primitive values and object reassignment, it is no longer needed. - @wire — Connects a property or function to a Salesforce data source (Apex method, User ID, record data, etc.). The framework calls the wire service reactively whenever the parameter changes.
// decoratorExamples.js
import { LightningElement, api, track, wire } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
export default class DecoratorExamples extends LightningElement {
// @api — public property, parent can set this
@api recordId;
// @track — deep observation of nested object
@track filters = { status: 'Active', type: 'Customer' };
// @wire — reactive connection to Apex
@wire(getContacts, { accountId: '$recordId' })
contacts;
// @api — public method, parent can call this
@api
refresh() {
// imperative logic here
}
handleFilterChange() {
// Mutating a nested property — @track detects this
this.filters.status = 'Inactive';
}
}
@api makes a property public — it creates a contract between the component and its parent. The parent can read and write the property. A property decorated with @api is automatically reactive: when the parent changes its value, the child re-renders.
@track affects reactivity within the component itself. It tells the LWC engine to perform deep observation on an object or array, so that mutations to nested fields (e.g., this.obj.nested.value = 'x') also trigger re-render. Without @track, only top-level reassignment (e.g., this.obj = {...}) triggers re-render.
Important: @api and @track are mutually exclusive — you cannot put both on the same property. Also, @api properties must never be mutated by the component itself (that violates one-way data binding). Always fire an event up to the parent and let the parent update the value.
LWC provides the following lifecycle hooks, called in this order:
- constructor() — Called when the component is instantiated. The shadow DOM does not exist yet. Do not access child elements or
this.templatehere. Always callsuper()first. - connectedCallback() — Called when the component is inserted into the DOM. This is where you subscribe to events, initialize data, or wire up platform event subscriptions (Lightning Message Service). The template is not rendered yet.
- render() — (Optional) Override to conditionally return different HTML templates. Not commonly used but available.
- renderedCallback() — Called after every render. This is the earliest point where you can safely access child elements via
this.template.querySelector(). Be careful — it fires on every re-render, so guard your logic with flags if you only want to run it once. - disconnectedCallback() — Called when the component is removed from the DOM. Use this to unsubscribe from events, clear timers, or release resources to avoid memory leaks.
- errorCallback(error, stack) — Called when a descendent component throws an error during the lifecycle. Acts like an error boundary. Use it to display a friendly error state.
You pass data from parent to child by binding to a public (@api) property on the child. In the parent's HTML template, you set the child component's attribute to a value from the parent's JavaScript. This is classic one-way, top-down data flow.
// childComponent.js
import { LightningElement, api } from 'lwc';
export default class ChildComponent extends LightningElement {
@api accountName; // public — parent sets this
@api accountId;
}
// childComponent.html
<template>
<p>Account: {accountName}</p>
</template>
// parentComponent.html
<template>
<!-- Parent binds its own property to child's @api property -->
<c-child-component
account-name={selectedAccountName}
account-id={selectedAccountId}>
</c-child-component>
</template>
accountName in JS becomes account-name in HTML. This is a frequent source of bugs for beginners.
The child cannot directly modify the parent's state. Instead, the child fires a custom DOM event using the CustomEvent API, and the parent listens for that event with an event handler. The child can include data in the event's detail property.
// childComponent.js — fires an event upward
import { LightningElement } from 'lwc';
export default class ChildComponent extends LightningElement {
handleButtonClick() {
const selectedValue = 'some-value';
// Dispatch a custom event with data in detail
this.dispatchEvent(new CustomEvent('select', {
detail: { value: selectedValue }
}));
}
}
// parentComponent.html — listens for the child's event
<template>
<c-child-component onselect={handleChildSelect}></c-child-component>
<p>Selected: {chosenValue}</p>
</template>
// parentComponent.js — receives the event
import { LightningElement, track } from 'lwc';
export default class ParentComponent extends LightningElement {
chosenValue = '';
handleChildSelect(event) {
this.chosenValue = event.detail.value;
}
}
Shadow DOM is a browser standard that encapsulates a component's internal structure and styles from the rest of the page. Each LWC component gets its own shadow root, meaning styles defined inside one component do not leak out to other components, and global styles do not leak in.
Practical implications for LWC developers:
- CSS is scoped: A CSS rule in
myComponent.cssonly applies to elements inside that component's template. You cannot accidentally override styles in child components. - Cross-component styling: To style child components from a parent, you must use CSS custom properties (variables) that the child explicitly exposes. Or use SLDS utility classes, which are loaded globally.
- querySelector is scoped:
this.template.querySelector()only searches within the component's own shadow root. You cannot use it to reach into a child component's internals. - The :host selector: Use
:hostin your CSS to style the component element itself from within.
This encapsulation is what makes LWC components safe to compose and reuse without style conflicts — a major advantage over Aura, which had no true style isolation.
The @wire decorator connects a component property or function to a reactive data source. The LWC wire service manages the lifecycle of the wire connection — it calls the data source when the component renders and whenever a reactive parameter (prefixed with $) changes.
When wired to a property, the framework populates an object with { data, error }. When wired to a function, the framework calls the function with that same object as an argument.
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import getRelatedContacts from '@salesforce/apex/AccountCtrl.getRelatedContacts';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
export default class WireExamples extends LightningElement {
@api recordId;
// Wire to a UI API adapter — auto-fetches when recordId changes
@wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME_FIELD] })
account;
get accountName() {
return getFieldValue(this.account.data, ACCOUNT_NAME_FIELD);
}
// Wire to an Apex method — $ prefix makes it reactive
@wire(getRelatedContacts, { accountId: '$recordId' })
wiredContacts({ data, error }) {
if (data) {
this.contacts = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.contacts = undefined;
}
}
}
@wire must be annotated with @AuraEnabled(cacheable=true). Without cacheable=true, the wire service will throw an error. Cacheable methods cannot perform DML operations.
Section 2: Events and Communication (Q9–Q15)
Standard DOM events (like click, change, focus) are predefined by the browser. They bubble up the DOM automatically and carry browser-specific data.
CustomEvent is a browser API that lets you create your own named events with arbitrary data in the detail property. In LWC, you use CustomEvent for all child-to-parent communication.
Key options when constructing a CustomEvent:
- bubbles: true — The event bubbles up through the DOM tree. By default, LWC events do not bubble.
- composed: true — The event crosses shadow boundaries. Required if you want the event to bubble past the shadow root of the component that dispatched it. Use with caution — it makes events global.
- detail — The payload. Can be any value (string, object, array). Keep it serializable for best practice.
For direct parent-child communication, use a non-bubbling, non-composed event. Only use bubbles: true, composed: true when you genuinely need an event to propagate to an ancestor far up the tree.
Imperative Apex calls are function calls you make yourself in JavaScript, as opposed to @wire which is automatic. You import the Apex method and call it like a regular async function. It returns a Promise.
Use imperative calls when you need full control over when the call fires — typically on button clicks, form submissions, or any user action.
// AccountController.cls (Apex)
public with sharing class AccountController {
@AuraEnabled
public static List<Contact> getContacts(Id accountId) {
return [SELECT Id, Name, Email FROM Contact
WHERE AccountId = :accountId LIMIT 50];
}
}
// myComponent.js (LWC — imperative call)
import { LightningElement, api } from 'lwc';
import getContacts from '@salesforce/apex/AccountController.getContacts';
export default class MyComponent extends LightningElement {
@api recordId;
contacts = [];
error;
isLoading = false;
handleLoadContacts() {
this.isLoading = true;
this.error = undefined;
getContacts({ accountId: this.recordId })
.then(result => {
this.contacts = result;
})
.catch(error => {
this.error = error.body?.message || 'An error occurred';
})
.finally(() => {
this.isLoading = false;
});
}
}
@wire is declarative and automatic. The framework calls the Apex method when the component renders and whenever a reactive property (prefixed with $) changes. The method must be cacheable=true (read-only). You cannot trigger it manually on a button click.
Imperative gives you full control. You call the method yourself in a handler function. It can be a non-cacheable method (so DML is allowed, though you should not do DML in LWC controllers — use service layers). You handle loading states and errors yourself with .then() / .catch() or async/await.
Use @wire for data that should load automatically with the component and refresh when an ID changes. Use imperative calls for actions triggered by the user, or when you need to handle loading/error state explicitly in your UI.
For @wire (property style): Check this.wiredResult.error. In your template, use lwc:if={error} to show an error message.
For @wire (function style): The wire handler receives { data, error }. Set a component property when error is truthy.
For imperative calls: Use .catch(error => ...) or a try/catch with async/await. The error object structure from Apex is: error.body.message for user-friendly messages, error.body.stackTrace for debug details.
Always display user-friendly messages. Use the reduceErrors utility function from @salesforce/apex (available in the LWC Recipes repo) to normalize errors from different sources into a string array.
Pubsub (publish-subscribe) is a communication pattern for components that do not have a direct parent-child relationship — for example, two sibling components or two components in completely different parts of the page.
Salesforce provides Lightning Message Service (LMS) as the official, Salesforce-supported solution for cross-component communication across different DOM trees, including Aura-to-LWC and Visualforce-to-LWC communication.
LMS uses a MessageChannel metadata file. Components subscribe with subscribe() and publish with publish() from lightning/messageService. Always unsubscribe in disconnectedCallback() to prevent memory leaks.
Avoid the old community pubsub JavaScript module — it was a workaround before LMS existed. Use LMS for all production pubsub needs.
You access DOM elements using this.template.querySelector() or this.template.querySelectorAll(). These methods are scoped to the component's own shadow root — they cannot reach into child component internals.
Only access the DOM in renderedCallback() or in event handlers — never in constructor() or connectedCallback(), as the template has not rendered yet.
To access an element in a child component, use @api to expose a method on the child, then call it via a template ref: this.template.querySelector('c-child').myPublicMethod().
Use the lwc:ref directive (available Spring '23+) as a cleaner alternative to querySelector for named elements: this.refs.myElement.
Slots are placeholders in a child component's template that the parent can fill with its own HTML content. They enable composition — building wrapper or container components that accept arbitrary content from the outside.
A default slot (<slot></slot>) accepts any markup placed between the child component's tags in the parent. A named slot (<slot name="header"></slot>) accepts markup with a matching slot="header" attribute in the parent.
Slots are especially useful for building reusable card, modal, or panel components where the shell is always the same but the inner content varies per use case.
Section 3: Performance and Best Practices (Q16–Q20)
Use the lwc:if, lwc:elseif, and lwc:else directives (introduced in Spring '23). These directives add or remove elements from the DOM based on a boolean expression.
<template>
<template lwc:if={isLoading}>
<lightning-spinner></lightning-spinner>
</template>
<template lwc:elseif={hasError}>
<p class="error">{errorMessage}</p>
</template>
<template lwc:else>
<!-- main content -->
</template>
</template>
Elements inside a false lwc:if block are removed from the DOM entirely (not just hidden). This is important to understand — lifecycle hooks like connectedCallback and disconnectedCallback fire as elements are added and removed.
The old if:true={condition} and if:false={condition} directives are deprecated as of Spring '23. They had several limitations: you could not chain else-if conditions, and you could not use if:false and if:true together on sibling templates without duplicating logic.
The new lwc:if / lwc:elseif / lwc:else syntax is cleaner, matches standard programming constructs, and eliminates the need for negation helper properties in JavaScript. New code should always use lwc:if. The old syntax still works but produces deprecation warnings in the LWC compiler.
Use the for:each directive to iterate over an array. You must provide a key attribute on the direct child element inside the loop. The key must be unique and stable (typically the record ID). The current item and its index are bound via for:item and for:index.
<template>
<ul>
<template for:each={contacts} for:item="contact" for:index="idx">
<li key={contact.Id}>
{idx}. {contact.Name} — {contact.Email}
</li>
</template>
</ul>
</template>
Alternatively, use the iterator:it={array} directive when you need to know if an item is the first or last in the list (it.first, it.last). The key attribute is still required with iterator.
Salesforce Lightning Design System (SLDS) is the CSS framework that defines the visual language for all Salesforce UIs. It provides utility classes, component blueprints, design tokens, icons, and spacing/typography rules so your custom components look native on the Salesforce platform.
In LWC, SLDS is automatically available — you do not need to import it. Apply SLDS classes directly in your template HTML. For example, slds-card, slds-button, slds-text-heading_medium, slds-m-around_medium.
Best practice: Use SLDS utility classes instead of writing custom CSS whenever possible. This keeps your UI consistent with Salesforce's design system and works correctly across different themes and accessibility modes. When you do write custom CSS, use SLDS design tokens (CSS custom properties like var(--lwc-colorTextDefault)) so your component respects theme changes.
Navigation mixins (NavigationMixin) from lightning/navigation allow LWC components to navigate to other pages within Salesforce — record pages, list views, tabs, external URLs, named pages, and more. They work across Lightning Experience, Salesforce Mobile App, and Experience Cloud sites.
To use them: mix NavigationMixin into your component class, then call this[NavigationMixin.Navigate](pageReference) where pageReference is an object describing the destination.
import { LightningElement } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
export default class NavExample extends NavigationMixin(LightningElement) {
navigateToRecord(recordId) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: recordId,
actionName: 'view'
}
});
}
navigateToNewRecord() {
this[NavigationMixin.Navigate]({
type: 'standard__objectPage',
attributes: { objectApiName: 'Contact', actionName: 'new' }
});
}
}
Section 4: Advanced Questions (Q21–Q25)
LWC components are tested with Jest, configured through @salesforce/sfdx-lwc-jest. Tests live in a __tests__ folder inside the component directory. The testing pattern is: create a component with createElement(), append it to the document body, interact with it, and assert on the rendered DOM.
- Use
jest.mock()to mock Apex imports and wire adapters. - Use
@salesforce/wire-service-jest-utilto emit wire data and errors in tests. - Use
Promise.resolve()after DOM interactions to wait for the microtask queue to flush (LWC renders asynchronously). - Always call
document.body.removeChild(element)inafterEach()to clean up.
Jest tests run in Node.js with jsdom — they do not require a Salesforce org, which makes them fast. Aim for high coverage on component logic, getter functions, event handlers, and error states.
@salesforce/schema is a scoped module that lets you import Salesforce object and field API names as typed references rather than raw strings. This gives you compile-time validation — if the object or field does not exist, the tooling will catch it before deployment.
import ACCOUNT_OBJECT from '@salesforce/schema/Account'; import NAME_FIELD from '@salesforce/schema/Account.Name'; import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry'; // These are ObjectInfo and FieldInfo references, not strings. // Use them with getRecord, getFieldValue, lightning-record-form, etc.
Always use @salesforce/schema imports instead of hardcoded string field names. If a field is renamed or deleted, your deployment will fail with a clear error rather than silently breaking at runtime.
Base components are a library of pre-built LWC components provided by Salesforce under the lightning namespace. They implement common UI patterns following SLDS design, are accessible, mobile-responsive, and maintained by Salesforce. Examples include:
lightning-input— text, email, number, date, checkbox, and other input typeslightning-button,lightning-button-iconlightning-card— standard card containerlightning-datatable— sortable, selectable data grid with inline editinglightning-combobox,lightning-radio-group,lightning-checkbox-grouplightning-record-form,lightning-record-view-form,lightning-record-edit-formlightning-spinner,lightning-badge,lightning-icon
Always prefer base components over custom-built equivalents. They handle accessibility, theming, and mobile automatically. Building custom alternatives costs significant development time and risks accessibility gaps.
LWC uses a reactive property system. When a property's value changes, the framework schedules a re-render of the parts of the template that depend on that property. Reactivity is automatic for:
- Primitive values (string, number, boolean) — any assignment triggers re-render.
- Object and array reassignment — replacing the reference triggers re-render.
- Properties decorated with
@track— mutations to nested fields trigger re-render. - Properties decorated with
@api— the parent updating a value triggers re-render in the child.
What does NOT trigger reactivity:
- Mutating a nested property of an untracked object:
this.obj.child.name = 'x'— use@trackor reassign the top-level reference. - Mutating an array's contents without reassignment (e.g.,
this.items.push(x)) — reassign:this.items = [...this.items, x]or use@track.
LWC batches DOM updates — multiple property changes in the same synchronous code path result in a single re-render, not one re-render per assignment.
This is a common question when interviewing at companies that use both ecosystems. Key differences:
- Platform vs universal: LWC is purpose-built for Salesforce and runs inside the Salesforce platform. React is a general-purpose library that runs anywhere JavaScript runs.
- Shadow DOM: LWC uses native Shadow DOM for style encapsulation. React does not use Shadow DOM by default (it uses a virtual DOM for efficient re-rendering).
- Data fetching: LWC has the wire service for declarative data binding to Salesforce APIs. React uses hooks like
useEffect+fetchor libraries like React Query / SWR. - State management: React has useState, useReducer, Context API, and an ecosystem of state libraries (Redux, Zustand). LWC has reactive properties and Lightning Message Service.
- Templating: LWC uses an HTML-based template file with Salesforce-specific directives. React uses JSX — JavaScript with embedded HTML-like syntax.
- Salesforce data access: LWC has built-in access to Apex, UI API, schema imports, platform events, and the full Salesforce data model. React has none of this without significant custom integration.
If you know React, LWC will feel familiar in concept (component model, one-way data flow, event handling) but different in syntax and the strong platform integration it provides.
Practice LWC Problems on ApexArena
Reading is not enough — cement your understanding by solving real LWC and Apex coding challenges in a browser-based Salesforce environment.
Start Practicing Free