Salesforce Interview Questions & Answers Book is now available!

Grab Now

JavaScript for LWC

Learn JavaScript Before LWC: The 12 Concepts That Matter

The 12 modern JavaScript concepts Lightning Web Components rely on, each with a tiny example and the exact place it shows up in LWC. Learn these, skip the rest.

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

TL;DR

  • You don't need all of JavaScript to write Lightning Web Components. You need about 12 modern concepts, and you'll use every one of them in your first real component.
  • The 12: modules, classes, getters, arrow functions and this, promises and async/await, destructuring and spread, array methods, events and CustomEvent, the Shadow DOM, immutability for reactivity, optional chaining, and template literals.
  • Most beginner LWC bugs are really JavaScript bugs: a lost this, an array mutated in place, a wired object you tried to edit, or logic stuffed into the template.
  • Learn each concept in plain JavaScript for an evening, then rebuild it inside a component. The free JavaScript for LWC course follows that order.

Before you learn Lightning Web Components, learn these 12 JavaScript concepts: modules, classes, getters, arrow functions and this, promises and async/await, destructuring and spread, array methods, events and CustomEvent, the Shadow DOM, immutability for reactivity, optional chaining, and template literals. That's the whole list. LWC is standard modern JavaScript with a few Salesforce additions, and these 12 ideas show up in nearly every component you'll write. Below, each one gets a tiny example and the exact spot it appears in LWC.

I see the same pattern with Apex developers and admins moving to LWC. They jump straight into components, copy an example, and get stuck on bugs that have nothing to do with Salesforce: this is undefined, the list doesn't re-render, or a wired record throws when they try to edit it. All of those are JavaScript problems. Fix the JavaScript first and LWC becomes much easier.

What you can skip for now

Just as useful is knowing what you don't need before your first component:

  • DOM manipulation libraries such as jQuery. LWC renders the DOM for you.
  • Front-end frameworks. You don't need React or Vue first, though the ideas carry over.
  • Prototype chains, generators and var hoisting trivia. Interesting, but they're rarely the cause of an LWC bug.
  • Build tools such as Webpack. The platform compiles your components.

Learn those later if you're curious. They won't block you.

1. Modules: import and export

Every LWC file is an ES module. You import the base class and the platform features, and you export your component.

// c/formatUtils/formatUtils.js — a service component with no template
export function formatPercent(value) {
    return `${Math.round(value * 100)}%`;
}
import { LightningElement } from 'lwc';
import { formatPercent } from 'c/formatUtils';
import getOpenCases from '@salesforce/apex/CaseController.getOpenCases';
import STATUS_FIELD from '@salesforce/schema/Case.Status';

Where it shows up in LWC: the top of every component. The @salesforce/apex and @salesforce/schema imports are how you reach Apex and fields. Importing fields through @salesforce/schema, rather than typing strings, lets the platform know your component depends on that field.

2. Classes and extends

A component is a class that extends LightningElement.

export default class CaseList extends LightningElement {
    cases = [];
    error;

    constructor() {
        super(); // must come first
    }
}

Where it shows up: every component. Know how class fields work (cases = [];), that super() must be called first in a constructor, and that lifecycle hooks such as connectedCallback() are simply methods on the class. You'll rarely need a constructor at all. Class fields and connectedCallback() cover most setup.

3. Getters

A getter is a method that reads like a property.

get hasCases() {
    return this.cases.length > 0;
}
<template lwc:if={hasCases}>
    <p>You have open cases.</p>
</template>

Where it shows up: constantly, because LWC templates don't take expressions. You can't write a comparison or a calculation inside the template, so you compute it in a getter and bind the getter by name. I think that's a feature, not a limitation: the logic lives in JavaScript, where you can read it, debug it and test it. There's a full post on this at why LWC templates use getters, not expressions.

4. Arrow functions and this

A regular function gets its own this. An arrow function uses the this of the code around it.

loadCases() {
    getOpenCases()
        .then((result) => {
            this.cases = result; // `this` is the component
        })
        .catch((error) => {
            this.error = error;
        });
}

Write that callback as function (result) { this.cases = result; } and it fails, because inside a plain function callback this is undefined in module code.

Where it shows up: in promise callbacks, array methods and anything you pass as a callback. Handlers you reference from the template, such as onclick={handleClick}, are called with this set to the component, so they work as normal methods. The trap is the callbacks you write inside those methods.

5. Promises and async/await

A call to an imperative Apex method returns a promise. async/await lets you write the same thing in order, top to bottom.

async handleRefresh() {
    this.isLoading = true;
    try {
        this.cases = await getOpenCases();
        this.error = undefined;
    } catch (error) {
        this.error = error;
    } finally {
        this.isLoading = false;
    }
}

Where it shows up: imperative Apex calls, refreshApex(), navigation helpers and anything that waits on the server. The finally block matters. Without it, a failed call leaves your spinner running forever, which is one of the most common LWC bugs I see in code reviews.

6. Destructuring and spread

Destructuring pulls values out of an object. Spread copies them into a new one.

import { getRecord, getFieldValue } from 'lightning/uiRecordApi';

// inside the component class
@wire(getRecord, { recordId: '$recordId', fields: [STATUS_FIELD] })
wiredCase({ error, data }) {
    if (data) {
        this.status = getFieldValue(data, STATUS_FIELD);
    } else if (error) {
        this.error = error;
    }
}
const updated = { ...original, Status: 'Closed' };

Where it shows up: wire functions receive { error, data } and you'll destructure that in nearly every component. Spread is how you work with wired data, which is read-only. You can't add a property to a wired record, but you can copy it and add the property to the copy.

7. Array methods: map, filter, find, reduce

These four replace most of the loops you'd write in Apex.

get rows() {
    return this.cases
        .filter((record) => record.Priority === 'High')
        .map((record) => ({
            ...record,
            recordUrl: `/lightning/r/Case/${record.Id}/view`
        }));
}

Where it shows up: shaping Apex results for lightning-datatable, which often needs extra columns such as URLs. It also shows up in counting, totalling and finding a selected row in event handlers. Notice that map plus spread builds new objects, which is exactly what read-only data needs.

8. Events and CustomEvent

Data goes down through public properties. Notifications come up through events.

// child: c-case-tile
handleSelect() {
    this.dispatchEvent(
        new CustomEvent('select', { detail: { caseId: this.caseRecord.Id } })
    );
}
<c-case-tile case-record={caseRecord} onselect={handleCaseSelect}></c-case-tile>
// parent
handleCaseSelect(event) {
    const { caseId } = event.detail;
    this.selectedId = caseId;
}

Where it shows up: every parent-child pair of components. Keep event names lowercase with no hyphens, listen with on plus the name, and put data in detail. For components that aren't related in the DOM, Lightning Message Service takes over. But understand plain DOM events first, because LMS makes much more sense once you do.

9. The Shadow DOM idea

Each component renders inside its own shadow tree. Its markup and CSS are encapsulated. A parent can't reach into a child's internals, and a child's styles don't leak out.

renderedCallback() {
    const input = this.template.querySelector('lightning-input');
    // this.template = your own shadow tree, not the whole page
}

Where it shows up: this.template.querySelector only finds elements in your own template. Your CSS only styles your own markup. Events stop at the shadow boundary unless you set bubbles and composed. Most "why can't I select that element?" and "why won't my CSS apply?" questions are the shadow DOM doing its job. As a rule, don't use composed: true unless you really need an event to cross component boundaries.

10. Immutability for reactivity

LWC re-renders when a field is reassigned. Changing an array or object in place doesn't count.

cases = [];

addCase(newCase) {
    // this.cases.push(newCase); — the template won't update
    this.cases = [...this.cases, newCase];
}

removeCase(caseId) {
    this.cases = this.cases.filter((record) => record.Id !== caseId);
}

Where it shows up: any list that changes after load. LWC tracks reassignment of fields. To make it watch changes inside an object or array, you'd need @track. Most of the time, building a new array with spread, map or filter is cleaner and is what I recommend. It's the same habit React developers learn, for the same reason.

11. Optional chaining and nullish coalescing

?. stops at null or undefined instead of throwing. ?? provides a fallback only for null or undefined.

get accountName() {
    return this.caseRecord?.Account?.Name ?? 'No account';
}

Where it shows up: relationship fields from Apex. If a case has no account, Account simply isn't in the result. Without ?. your getter throws during render and the component breaks. Use ?? rather than || when 0 or an empty string is a valid value.

12. Template literals

Backtick strings with ${} placeholders let you build strings without concatenation.

get tileClass() {
    return `slds-box slds-m-bottom_small ${this.isSelected ? 'slds-theme_shade' : ''}`;
}

get summary() {
    return `${this.cases.length} open cases`;
}
<div class={tileClass}>
    <p>{summary}</p>
</div>

Where it shows up: dynamic CSS classes, labels, URLs and messages. Because the template can't evaluate expressions, a getter that returns a template literal is the standard way to build a conditional class or a formatted label.

Putting the 12 together

Here's a small component that uses most of them. Notice the template: it only binds names, never expressions.

import { LightningElement, api, wire } from 'lwc';
import getOpenCases from '@salesforce/apex/CaseController.getOpenCases';

export default class AccountCases extends LightningElement {
    @api recordId;
    cases = [];
    error;

    @wire(getOpenCases, { accountId: '$recordId' })
    wiredCases({ error, data }) {
        if (data) {
            this.cases = data.map((record) => ({ ...record, label: `${record.CaseNumber}: ${record.Subject}` }));
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.cases = [];
        }
    }

    get hasCases() {
        return this.cases.length > 0;
    }

    get countLabel() {
        return `${this.cases.length} open`;
    }
}
<template>
    <lightning-card title="Open cases">
        <p slot="actions">{countLabel}</p>
        <template lwc:if={hasCases}>
            <template for:each={cases} for:item="item">
                <p key={item.Id} class="slds-p-horizontal_small">{item.label}</p>
            </template>
        </template>
        <template lwc:else>
            <p class="slds-p-horizontal_small">No open cases.</p>
        </template>
    </lightning-card>
</template>

The Apex method behind it would be an @AuraEnabled(cacheable=true) method in a with sharing class that queries with WITH USER_MODE. That way the component only ever shows cases the running user can see.

The component uses modules, a class, getters, an arrow callback, destructuring, spread, map, the reactive $recordId parameter and template literals. That's most of the list in about 40 lines.

LWC bugs that are really JavaScript bugs

When a component misbehaves, check this table before you blame the platform. Every row is a question I've seen asked many times, and every answer is one of the 12 concepts.

SymptomUsual causeConcept
"Cannot set properties of undefined" inside a .then()A plain function callback lost this4. Arrow functions
The list doesn't update after you add an itemThe array was mutated in place with push10. Immutability
Setting a property on a wired record throws an errorWired data is read-only and must be copied first6. Spread
The spinner never stops after an errorNo finally around the await5. Promises
The component breaks only for some recordsA missing relationship field without ?.11. Optional chaining
querySelector returns null for a child's inputYou reached into another component's shadow tree9. Shadow DOM
The parent never hears the child's eventEvent name has capitals or hyphens, or the on handler is misspelled8. CustomEvent
The template won't compile because of a comparisonLogic written in the template3. Getters

Most of these errors come from the browser, not from Salesforce, which is the giveaway. Search for them as JavaScript problems and you'll find the answer faster.

How I'd practise these

For each concept, spend one evening on plain JavaScript in the browser console or a small file, until you can write the example from memory. Then rebuild it inside a component in a Developer Edition org. The browser console teaches you the language. The org teaches you where the language meets the platform.

Then break things on purpose. Remove the arrow function and watch this disappear. Push to an array and watch the template not update. Try to edit a wired record and read the error. Every bug you cause deliberately is one you'll recognise in seconds when it happens for real.

Where to learn this, free

I built a free course around exactly this list, taught the way I teach everything: typed live, explained line by line, no slides.

Javascript for Lightning Web Components [FREE]

22 episodes · 2.6 hours

Free — the modern JavaScript LWC relies on

Free course

When you're ready for components, Lightning Web Components Development picks up where the JavaScript course leaves off: events, Lightning Data Service, LWC with Apex, navigation, Lightning Message Service and testing. The LWC learning guide shows the full path, and LWC interview questions is a good way to check whether the concepts have stuck.

Learn it by building it

Every course, every code repository, the discussion group and certificates of completion — one subscription.

Frequently asked questions

Do I need to learn JavaScript before LWC?

Yes, at least the modern core. Lightning Web Components are standard JavaScript classes and modules, so if classes, promises, arrow functions and array methods are unfamiliar, every LWC tutorial will feel like magic you can't debug.

How long does it take to learn enough JavaScript for LWC?

For an admin or Apex developer who already thinks in code, a few focused weeks on these 12 concepts is usually enough to start building components. You don't need DOM manipulation libraries, frameworks or deep prototype theory first.

Is LWC JavaScript different from normal JavaScript?

The language is the same modern JavaScript. LWC adds decorators such as @api and @wire, special import paths such as @salesforce/apex and @salesforce/schema, and some security and template rules on top of it.

Can I use React knowledge in LWC?

Partly. Components, one-way data flow and immutability carry over well. JSX does not: LWC templates are HTML that bind properties and getters, without inline JavaScript expressions.

Is the JavaScript for LWC course free?

Yes. JavaScript for Lightning Web Components is one of the completely free courses on Salesforce Casts, so you can learn the foundations before deciding on a paid plan.

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