Module 2 — ANSWER SHEET (SEALED)
Companion to 02_Topic02_LWC_UI_JS.md — open ONLY after you have written your own attempt.
Protocol (file 00, M1→M2): write ≥2 hypotheses + 2 solution attempts before reading. Then compare your attempt against this sheet like an investigator compares a suspect's story to the evidence. The contrast IS the learning. Then REDO from memory, then do the drill.
INCIDENT 1 — THE INFINITE RENDER LOOP
The problem restated
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"?
Model answer (2-min interview version)
- The assassin:
this.isLoading = false;insiderenderedCallback().renderedCallbackfires after every render; assigning a tracked property there schedules a re-render →renderedCallbackagain → infinite loop → the browser thread never yields → "Page Unresponsive." (The spinner'sif:trueguaranteed a DOM change on first assignment, so the first re-render was certain.) - The fix — two patterns:
- One-time init: guard flag set BEFORE the work:
renderedCallback() { if (this.chartInit) return; this.chartInit = true; this.initChart(); // DOM exists here }- Data-driven redraws: never through
renderedCallbackstate. Use an@apisetter (or the@wirecallback) that callsthis.redraw()only when the chart already exists.
- Rule to state: reactive state changes belong in event handlers,
connectedCallback, and imperative flows — never inrenderedCallbackfor control flow.
Self-grade — you "got it" if you named:
-
renderedCallbackfires after every render - Reactive assignment → re-render → loop (the mechanism chain)
- Guard flag pattern (set before the work)
-
connectedCallback≠ DOM work (chart div doesn't exist yet) - "Redraw on data change" via
@apisetter / wire callback, not renderedCallback
THE REDO — model answer
export default class AccountChart extends LightningElement {
chartInit = false;
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [...] })
account({ data, error }) {
if (data) this.redraw(data); // redraw path — chart exists after first render
}
renderedCallback() {
if (this.chartInit) return; // guard
this.chartInit = true;
this.initChart(); // one-time DOM init
}
}No loop: renderedCallback never assigns reactive state; data changes flow through the wire callback, which only redraws.
RETRIEVAL DRILL — model answers
- Parent→child lifecycle order: Parent
constructor→ ParentconnectedCallback→ Childconstructor→ ChildconnectedCallback→ ChildrenderedCallback→ ParentrenderedCallback. - Why is reactive assignment in
renderedCallbackdangerous? → It fires after every render; the assignment marks a property dirty → schedules a re-render → the hook fires again → infinite render loop / frozen tab. - Two things you may NOT do in
constructor: access DOM (none exists) and call Apex/@wirelogic (and anysuper()must be the first statement; properties aren't set yet). - What is
errorCallbackfor, and what does it NOT catch? → Catches errors thrown in descendant components (error boundary, fallback UI). It does NOT catch errors in your own code or event handlers. - Is
@trackrequired for a plain object field? → No — all fields are reactive since Spring '20; shallow reactivity is default since Winter '21 (API 49).@trackis only for deep observation of plain object/array internals, and it does NOT observe class instances,Date,Map,Set.
INCIDENT 2 — THE WIRE THAT NEVER FIRED
The problem restated
Wire works on record pages, dead on App/Experience pages, zero errors. Mechanism + robust fix.
Model answer (2-min interview version)
- Mechanism:
@wire(getRecord, { recordId: '$recordId', ... })— the$prefix makesrecordIda reactive input: the wire is deferred until the input has a defined value.@api recordIdis auto-populated only on record pages. On App pages, Experience Cloud pages, utility bars — no record context →recordIdstaysundefined→ the wire never fires. Not an error: a deferred provisioning that never resolves. That's why there's no console error and no Apex call. - Fix: detect no-record-context in
connectedCallback; fall back to the page reference (currentPageReference().state.recordId) when on an App page with a record in the URL; if still none → render an explicit empty state. Always render loading/empty/error states — never assume the wire fired. - Bonus (why "it's cached, refresh it" was wrong):
refreshApexonly works on data actually provisioned by@wire— a never-fired wire has nothing to refresh.
Self-grade — you "got it" if you named:
-
$reactive param + undefined = deferred forever -
recordIdpopulated only on record pages - No error/no Apex call = "never provisioned," not "cached"
- Fallback strategy (page reference / imperative / parent-provided)
- Explicit empty-state UX
THE REDO — model answer
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME_FIELD] })
wiredAccount;
connectedCallback() {
if (!this.recordId && this.pageRef?.state?.recordId) {
this.recordId = this.pageRef.state.recordId;
}
this.hasContext = !!this.recordId;
}$recordId = reactive: wire waits and re-provisions when it changes. Plain recordId = static: evaluated once, never re-runs on change.
RETRIEVAL DRILL — model answers
$input undefined at evaluation? → The wire does not fire (deferred until defined). Silently.- What must an Apex method have to be wireable? →
@AuraEnabled(cacheable=true). refreshApex— when does it work, when NOT? → Works on data provisioned by a@wire(Apex wire). Does NOT work on: cacheable methods called imperatively, never-provisioned wires, LDS adapters.- 3 situations where a wire re-provisions: reactive input changed;
refreshApexcalled; component re-rendered / page navigation. - Wire vs imperative for a Save button: imperative — DML can't be cacheable, and control/loading/error belong to the button handler.
INCIDENT 3 — THE STALE SCREEN
The problem restated
One side refreshes and stays stale; the other doesn't refresh at all and stays fresh. Explain the asymmetry.
Model answer (2-min interview version)
- Two cache worlds: the Apex
@wirecache (client-side cache ofcacheable=trueresults — invalidated only by reactive changes orrefreshApex) and the LDS / UI API cache (which auto-invalidates onupdateRecord/createRecord/deleteRecord). - The asymmetry: the right panel used LDS
getRecord→updateRecordauto-invalidated the LDS cache → it refreshed itself for free. The left panel used an Apex@wire→ the Apex cache is blind to imperative DML (saveRow) → stayed stale. - The hidden trap in the dev's
refreshApex: correct instinct for the left panel, BUT (a) if the wire result is mapped into a separate display array (this.rows = mapRows(data)), re-provisioning alone doesn't rebuild the table — you must re-map; (b) if the wire never provisioned (Incident 2) or the method was called imperatively,refreshApexis a silent no-op. - Fix:
// Left (Apex wire): after imperative save
await refreshApex(this.wiredSearch);
this.rows = mapRows(this.wiredSearch.data); // re-map!
// Right (LDS): updateRecord auto-invalidates; add for cross-component safety
notifyRecordUpdateAvailable([this.recordId]);Self-grade — you "got it" if you named:
- Two distinct caches (Apex wire cache vs LDS cache)
- LDS auto-invalidates on its own writes
- Apex wire cache blind to imperative DML → needs
refreshApex - Re-map display arrays after refresh
-
refreshApexsilent no-op conditions
THE REDO — model answer
Panels: search = Apex @wire (filtered multi-record — Apex territory); detail = LDS getRecord (single record, auto-fresh). Post-save: left → refreshApex(this.wiredSearch) + re-map; right → nothing (auto) + notifyRecordUpdateAvailable. Datatable bulk save: onsave → Apex Database.update(draftValues, false) → map SaveResult[] failures to the table's errors attribute.
RETRIEVAL DRILL — model answers
- Two cache systems + who invalidates: Apex
@wirecache —refreshApex/ reactive input change; LDS/UI API cache — auto on LDS writes +notifyRecordUpdateAvailable. refreshApex— 3 conditions to actually work: (1) data came from a@wire; (2) the wire has provisioned at least once; (3) you pass the wired property/function result (a Promise is returned — await it).- When does LDS auto-refresh other components? → When a record they read via
getRecordis written viaupdateRecord/createRecord/deleteRecord(cache invalidation propagates). - Why search stale, detail fresh? → The detail panel's LDS cache self-invalidates on save; the search panel's Apex wire cache has no idea an imperative method changed the data — it only re-provisions on
refreshApex. notifyRecordUpdateAvailable? → Manually invalidates the LDS cache for given record IDs — the belt-and-braces after LDS writes (or for changes made outside LDS).
INCIDENT 4 — THE FROZEN TABLE
The problem restated
Four defects in four lines: freeze, empty Owner column, correct paged design, and where the 50,000-row limit bites.
Model answer (2-min interview version)
- The freeze:
lightning-datatablerenders real DOM per row. Sweet spot ~1,000 rows × ~5 columns; >250 rows → stay <20 columns. 10–50K rows = browser death. "50,000 is legal" is a limit, not a design target. - Empty column: datatable has no nested-field support —
fieldName: 'Owner.Name'renders blank. Flatten: mapOwner.Name→OwnerNamein Apex or JS. - Paged design: Apex takes
offset/limit(ornextRecordsUrl); datatable usesenable-infinite-loading+onloadmore+load-more-offset(target.isLoadingtoggling); never render the table until data exists (spinner gate). - Where the row limit bites: 50,000 rows through a wire = multi-megabyte JSON (heap server-side, memory client-side) — plus the classic "passed in sandbox (400 rows), died in prod" replay. Bulk save: send only
event.detail.draftValues→Database.update(rows, false)→ mapSaveResult[]errors to the table.
Self-grade — you "got it" if you named:
- DOM scale / rows-rendered as the freeze cause
- No nested-field support → flatten
- Infinite scroll wiring (
enable-infinite-loading,onloadmore,load-more-offset) - Payload/heap cost of shipping 50K rows
-
draftValues-only bulk save + partial-success mapping
THE REDO — model answer
@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];
}columns = [{ label: 'Owner', fieldName: 'OwnerName', type: 'text' }];
handleLoadMore(event) {
const target = event.target;
target.isLoading = true;
this.offset += PAGE_SIZE;
this.rows = [...this.rows, ...(await getPage({ offset: this.offset, limit: PAGE_SIZE }))];
target.isLoading = false;
}Monitor: AsyncApexJob.TotalJobItems, AsyncApexJob.JobItemsProcessed, AsyncApexJob.Status.
RETRIEVAL DRILL — model answers
- Datatable perf sweet spot: ~1,000 rows × 5 columns; >250 rows → <20 columns; custom data types hurt perf.
- Why
fieldName: 'Owner.Name'renders nothing: datatable doesn't resolve nested/dot-notation fields — the column definition must be a flat property (OwnerName), which you produce in Apex/JS. - Two reasons "50,000 rows is legal" is the wrong bar: (1) DOM scale — rendering tens of thousands of rows freezes the browser; (2) payload — multi-MB JSON through a wire hits heap/serialization/memory limits and destroys load time.
onsavepayload:event.detail.draftValues(only changed rows) → pass those to Apex, not the whole table.- 3 loading-state patterns:
lightning-spinnergated byisLoading;target.isLoading = true/falsefor datatable infinite load; not rendering the datatable untildatais present.
INCIDENT 5 — THE LOST EVENT
The problem restated
Event fires (console proves it), the immediate parent doesn't hear it. Why, what does the architect want, and the difference.
Model answer (2-min interview version)
- Mechanism:
CustomEventdefaults arebubbles: false, composed: false→ the event stays inside the child's own shadow DOM; nothing outside hears it. The parent (and grandparent, in another shadow root) are deaf — theconsole.logonly proves the dispatch, not the travel. - The architect's fix (standards, one dispatch):
this.dispatchEvent(new CustomEvent('productselected', {
detail: this.product, // data rides in detail (retargeting-safe)
bubbles: true, // travel up the containment tree
composed: true // cross shadow boundaries → public API
}));- The rejected alternative: manually re-dispatching through every intermediate component — the Aura-era plumbing hack; fragile and redundant.
- When LMS instead: only for unrelated components (no shared hierarchy) — it's global pub/sub; every subscriber gets every message; harder to test; leaks if you forget
unsubscribeindisconnectedCallback. Events for hierarchies, LMS for strangers.
Self-grade — you "got it" if you named:
- Defaults
bubbles:false, composed:false -
bubbles= travel up the tree;composed= cross shadow DOM - One dispatch serves all ancestors
- Data in
event.detail, notevent.target(retargeting) - LMS = unrelated components only + unsubscribe in disconnectedCallback
THE REDO — model answer
Correct dispatch (above). Parent: onproductselected={handleSelected} reading event.detail. Judgment answer: filter panel ↔ results table across page areas with no shared parent → LMS with a message channel (lightning/messageService: publish/subscribe/unsubscribe, MessageContext), subscribed in connectedCallback, unsubscribed in disconnectedCallback.
RETRIEVAL DRILL — model answers
- Defaults:
bubbles: false, composed: false.bubbles= travels up the containment tree;composed= crosses shadow DOM boundaries. - Where data travels:
event.detail— becauseevent.targetis retargeted to the shadow host when read from outside the dispatching component's shadow root. - LMS vs CustomEvent decision rule: CustomEvent for related components in a hierarchy; LMS only for unrelated components that share no tree relationship.
- LMS subscriptions:
unsubscribeindisconnectedCallback(andunsubscribeAllon destroy) — otherwise memory leaks. - Aura application events → LWC: replaced by LMS; LWC has no application events (and no
$A, no two-way binding).
INCIDENT 6 — THE SEARCH THAT KILLED THE SERVER
The problem restated
Cacheable ≠ free. Three mechanisms turn typing into an attack. Rewrite the handler including the race condition.
Model answer (2-min interview version)
- No debounce: one request per keystroke (~6–7/sec). Fix:
setTimeout~300 ms after the last keystroke — one call per pause. - Cacheable only caches hits: each new character = a new argument = a cache miss = a real Apex execution ("salesforce" typed = up to 9 executions).
- Hidden server cost: request handling + SOQL + serialization every execution — and a leading-wildcard
LIKE '%...%'is a non-selective full scan (the CPU replay of Module 1 Incident 3, client-driven). - Race condition: a stale response ("s") arriving after the current one ("sa") renders wrong results — guard by rendering only when the response's term still equals the current input.
handleKeyChange(event) {
const term = event.target.value;
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(async () => {
this.isSearching = true;
try {
const results = await searchContacts({ term });
if (term === this.latestTerm) this.results = results; // race guard
} catch (err) {
this.error = reduceErrors(err);
} finally {
this.isSearching = false;
}
}, 300);
}Why "cacheable = cheap" is wrong: cacheable saves repeat reads of the same input and enables @wire sharing — it doesn't throttle, cache misses, or make LIKE '%...%' selective.
Self-grade — you "got it" if you named:
- Debounce (~300 ms, clearTimeout/setTimeout)
- Cache-miss-per-keystroke (new input = new execution)
- Server-side cost of each execution (serialization + non-selective LIKE)
- Race guard (render only latest term)
- Wire-with-reactive-term still needs debounce
THE REDO — model answer
The imperative handler above (debounce + race guard + error handling). If it were a @wire with reactive term: debounce stays (the wire re-provisions per change — actually you'd debounce the input into a debouncedTerm and bind the wire to $debouncedTerm); the race guard moves into the wiring (the wire guarantees latest-param ordering by re-provisioning, but stale data from a previous term is still possible if you cache-transform — render from the wire's data tied to the current term). Server-side regardless: escapeSingleQuotes, LIMIT, selective fields.
RETRIEVAL DRILL — model answers
- Why "cacheable = cheap" is wrong (3 reasons): (1) each new input is a cache miss → real execution; (2) no throttling — it doesn't debounce; (3) server cost per execution (serialization, non-selective
LIKEfull scan) regardless of caching. - Debounce pattern:
clearTimeout(this.timer); this.timer = setTimeout(() => { ... }, 300);capturing the value in a closure. Promise.allvsallSettled:allrejects fast on the first failure (parallel independent calls);allSettledwaits for every promise and reports per-promise status — use when one failure shouldn't kill the batch.- What does
fetchNOT reject on? → HTTP error statuses (404/500) — only network failures reject; checkresponse.ok. - NodeList → array:
[...nodeList]orArray.from(nodeList)(e.g.,[...this.template.querySelectorAll('lightning-input')]).
INCIDENT 7 — THE XSS IN THE "TRUSTED" TEXT
The problem restated
Three defense assumptions, all failed. Secure version + the LWS truth.
Model answer (2-min interview version)
- "It's just text" →
innerHTMLreparses attacker-controlled markup; XSS needs no<script>tag (<img src=x onerror=...>,javascript:URLs, SVG/foreign content). Regex sanitization of HTML is bypassable. The comment executed in every visitor's browser and (stored XSS) during authenticated sessions too. - "We use a CDN" → Lightning CSP blocks inline scripts and external CDN scripts. Legit libraries go in a Static Resource and load via
loadScript(this, resourceUrl + '/lib.js')/loadStylefromlightning/platformResourceLoader(dedupes, returns Promises). - "LWS blocks that" → LWS distorts, it doesn't block.
innerHTML,eval,Function,localStorage,document.cookieall run — inside a per-namespace sandboxed JS Realm. LWS limits blast radius (defense-in-depth); it doesn't neutralize the vulnerability. Truly blocked: Workers + a tiny set.
- Secure version: declarative template
{comment.text}(auto-escaping) orlightning-formatted-rich-textfor trusted rich text; server:with sharing,WITH USER_MODE,escapeSingleQuotes, input validation; Guest User least-privilege audit; Event Monitoring. - Cross-namespace gotcha: objects crossing LWS namespaces are Proxy-wrapped — in-place mutation silently doesn't propagate; JSON-serialize for
event.detail.
Self-grade — you "got it" if you named:
-
innerHTML+ untrusted data = XSS (no<script>needed) - CSP blocks inline + CDN → Static Resource +
loadScript - LWS distorts, not blocks (Realm sandbox)
- Declarative templates auto-escape
- Server-side: with sharing + USER_MODE + escapeSingleQuotes + least privilege
THE REDO — model answer
Template: <template><div class="c">{comment.text}</div></template> (auto-escaped). Library: import LIB from '@salesforce/resourceUrl/mylib'; + loadScript(this, LIB + '/mylib.js'). Apex: public with sharing class ... + WITH USER_MODE + escapeSingleQuotes. Audit: Guest User profile — remove legacy object access; enable Event Monitoring.
"Does LWS break localStorage?" → No — it runs, but inside the sandboxed per-namespace realm (distorted, isolated from the real global). The catch: it's containment, not permission — a payload can still read "its own" storage; don't put secrets in client storage.
RETRIEVAL DRILL — model answers
- LWS vs Locker: LWS = per-namespace JavaScript Realm sandbox that distorts APIs; default for orgs created Winter '23+; LWC. Locker = Aura-era monkey-patched DOM facades.
- CSP blocks: inline scripts + external CDN scripts. Legal load: Static Resource +
loadScript/loadStyle(lightning/platformResourceLoader). - Why LWS doesn't "fix" XSS: the payload still executes — LWS only contains the environment (sandboxed APIs, isolated realm). The vulnerability (untrusted HTML injection) remains.
- Two no-
<script>XSS vectors:<img src=x onerror="...">(event handler attribute) andjavascript:URLs (e.g.,<a href="javascript:...">); also SVG/foreign-content vectors. - Cross-namespace objects: wrapped in Proxies — in-place mutation is silently lost; workaround: serialize to JSON / structured clone when passing objects across namespaces (e.g., in
event.detail).
INCIDENT 8 — THE AURA GHOST
The problem restated
Who is right about embedding, who is right about the rule — and the migration strategy end-to-end.
Model answer (2-min interview version)
- The dev is wrong twice: (1) Aura embeds LWC; LWC cannot embed Aura. The new quoting widget ships as LWC inside the existing Aura parent (attributes in,
on*event handlers out) — no helper reuse needed, LWC is a fresh ES-module class. (2) "New code = LWC" is the 2026 standard; building new Aura is debt. Legitimate Aura exceptions: surgical in-place fixes to existing components, and areas where Aura is still required (parts of Service Console, some Experience Cloud/CRM Analytics overrides). - Migration strategy — leaves first, host last:
- Convert leaf/child components to LWC (the Aura parent keeps hosting them — interop is one-way, so the tree stays shippable).
- Translate in-place:
aura:attribute→@api;cmp.get/set→direct assignment;aura:if/else→lwc:if/elseif/else;aura:iteration→for:each+ uniquekey; component events→CustomEvent; application events→LMS;{!c.action}→JS method; controller/helper→one ES6 class; renderer overrides→guardedrenderedCallback. - Migrate the host last (re-architect as a flexipage/LWC composition), then delete the Aura bundle.
- Freeze new Aura work; track migration as technical debt.
- The concept with no LWC equivalent: Aura application events → replaced by LMS.
Self-grade — you "got it" if you named:
- Direction: Aura embeds LWC, never the reverse
- "New code = LWC" + the narrow exceptions
- Leaves first, host last
- The 7-row mapping table
- Application events → LMS (no equivalent)
THE REDO — model answer
Mapping table: aura:attribute→@api; cmp.get("v.x")/cmp.set→direct field assignment; aura:if/aura:else→lwc:if/lwc:elseif/lwc:else; aura:iteration→for:each + key; component events→CustomEvent (bubbles/composed); application events→LMS; {!c.action}→JS method; controller/helper→one ES6 class; renderer→guarded renderedCallback.
Direction: Aura can embed LWC; LWC cannot embed Aura.
3 phases: (1) freeze new Aura, (2) migrate leaves while Aura parents host LWC children, (3) re-architect hosts (flexipage/LWC) and delete Aura bundles.
RETRIEVAL DRILL — model answers
- Embedding direction: Aura embeds LWC; LWC cannot embed Aura.
aura:iterationandaura:if: →for:each+ uniquekey; →lwc:if/lwc:elseif/lwc:else.- Aura component events / application events: component events →
CustomEvent; application events → LMS (no direct LWC equivalent). - Why "migrate the host first" is wrong: the host is the only thing that can embed LWC children (interop is one-way) — migrating it first blocks all child migrations and breaks the shippable-tree strategy. Leaves-first keeps the system deployable at every step.
cmp.get("v.x")→ LWC: direct property access (this.x); assignment triggers reactivity — nocmp.set.
🏆 CAPSTONE — MODEL DESIGN CHECKLIST
The module's model design (part 2 of the capstone section) covers the full architecture. This sheet adds the two things you must be able to produce, not just read:
The 60-second spoken walkthrough (closed notes) — model script
"Three components:
contact-searchowns the state;c-contact-resultsandc-contact-pillsare presentational and communicate viaCustomEventwithbubblesandcomposed— the parent is the single source of truth for selection. Apex: one cacheablesearchmethod —with sharing,WITH USER_MODE,escapeSingleQuotes,LIMIT 25— and one non-cacheablesendEmailsthat batches into a singleSingleEmailMessage[]becausesendEmailcaps at 10 invocations per transaction. Search is imperative with a 300 ms debounce and a race guard — wire reactivity doesn't replace either. After the send InotifyRecordUpdateAvailableand re-run the last search to keep the list honest. Every Apex error flows throughreduceErrors, the send button is disabled while in flight, and I Jest-test the debounce, the race guard, and the dedupe."
The Jest test list (model sketch)
- Debounce: fire
handleKeyChange5× rapidly → assertsearchContactscalled once (fake timers). - Race guard: resolve the older request after the newer one → assert the newer result is rendered.
- Dedupe: click the same result twice → one pill.
- Pill removal: click pill →
removeevent → parent state updated. - Errors:
searchContactsrejects →reduceErrorspath renders the error state, no spinner stuck. - Send: button disabled while in flight; success/failure toasts fired.
INTERLEAVED PRACTICE SET — model answers
- Apex + LWC, one bug: missing
await refreshApex(this.wiredResults)after the save (plus re-mapping the table rows). The Module-1 limit the email-sending method must respect: 10sendEmailinvocations per transaction — batch into oneMessaging.SingleEmailMessage[]. - Choose the tool:
@wire getRecord(LDS) — single record, needs no Apex, auto-refreshes, enforces sharing/FLS. The related-list count is a multi-record aggregate — that part needs an Apex@wire(or a rollup summary). Split: LDS for the record, Apex wire for the count. - Live-code sketch: debounce lives in the input handler (
clearTimeout/setTimeout(300));refreshApex(this.wiredCounter)in the same handler's completion (after the debounced search resolves), so the count re-provisions from the wire cache. - Whiteboard: Module 1 OOE fires-more-than-once: workflow field-update re-fire (step 11) and triggers per 200-record chunk. Module 2:
connectedCallback(every DOM insertion) andrenderedCallback(every render). - Security double (ordered by severity): (1) fix the Apex:
with sharing+WITH USER_MODE+stripInaccessible+escapeSingleQuotes(stops the record/field leak — Module 1 Incident 8); (2) fix the render: declarative template instead ofinnerHTML(stops the stored XSS — Module 2 Incident 7); (3) audit Guest User profile (least privilege, remove legacy access); (4) enable Event Monitoring; (5) re-test with a Guest-User-run integration test.
Quick answer-key summary (print on one card)
- Lifecycle order: P ctor → P connected → C ctor → C connected → C rendered → P rendered.
- renderedCallback: after EVERY render — guard one-time init, never set reactive state.
$xundefined = wire never fires (silent).refreshApex= wire data only.- LDS auto-invalidates on its writes; Apex wire needs
refreshApex+ re-map. - CustomEvent:
bubbles: true, composed: truefor public cross-boundary events; data indetail. - LMS = unrelated components; unsubscribe in
disconnectedCallback; no application events in LWC. - Debounce ~300 ms + race guard; cacheable ≠ cheap (misses still execute).
- Datatable: flatten nested fields; ~1,000 rows × 5 cols; infinite scroll;
draftValuesonly. - LWS distorts, not blocks; CSP blocks inline/CDN → Static Resource +
loadScript. - Aura embeds LWC (never reverse); leaves first, host last; app events → LMS.