@api to accept data from a parent, @wire to stream data directly from Salesforce, and @track only when it needs to detect changes buried deeply inside complex objects or arrays.
⚡ 1-Minute Summary (Key Points)
- Always reach for standard LWC data services first. Write custom Apex only when the standard tools cannot do the job.
- Use
@wirefor reactive, automatically cached data streams. Use imperative Apex when YOU need to choose the exact moment the data is fetched (like after a button click). - Aura components can hold LWC components, but LWC cannot hold Aura. This dictates the order you must migrate legacy code.
- Modern LWC treats all standard fields as reactive by default when they are reassigned.
@trackis no longer needed on every variable.
๐บ️ Module Map: Standards-based UI on Salesforce data
MODULE 9 root: 'Standards-based UI on Salesforce data' ├─ Component model, lifecycle hooks, decorators (@api/@track/@wire) ← [ You are here ] ├─ Data: wire vs imperative Apex, LDS, refreshApex, caching ├─ Communication: parent↔child, sibling via LMS, custom events ├─ Performance: lazy load, virtualization, wire dedupe └─ Security: LWS/Locker, CSP, FLS in Apex controllers
๐ Shadow DOM & Reactivity Explained
LWC is built entirely on modern web standards. Each component's HTML template renders into a Shadow DOM (historically synthetic, but increasingly native under Lightning Web Security). This provides powerful encapsulation.
- CSS Isolation: Parent CSS styles do not leak in and override your component's styling.
- DOM Protection: Global JavaScript commands like
document.querySelectorcannot reach inside to manipulate your elements. - Reactivity by Default: In modern LWC, all standard fields are automatically reactive when you reassign them. If you change a string or number, the UI updates instantly.
- The Purpose of
@track: You only need@trackwhen you mutate the inside of a complex object or array (like changing one specific property on an object, or pushing a new item onto an existing list). - The Purpose of
@api: This marks a public property or method, turning it into a remote control that a parent component can use to pass data down.
this.searchKey = 'acme' changes a plain field and automatically re-renders the component. You do not need @track.this.filters.region = 'North' mutates a nested value inside an object. The UI will not repaint automatically unless you decorated filters with @track.Pro-tip: Instead of using
@track, the cleaner, modern pattern is to reassign the entire object using the spread operator: this.filters = { ...this.filters, region: 'North' };
@api as the parent's remote control to send data in, use @wire to create an auto-refreshing pipe to Salesforce data, and remember that plain fields are reactive by default when reassigned.
๐งญ 360 Card — Shadow DOM & Reactivity
@track only when you are mutating deeply inside an object or an array.๐ Gain: You get CSS style and DOM encapsulation completely for free. No poorly written code from another component can accidentally break yours.
๐ฐ Price: The sealed room cuts both ways. A global
document.querySelector cannot find your elements, and neither can standard third-party JavaScript UI libraries without complex workarounds.๐ Limits: Reassigning a variable triggers a render, but mutating it in-place (like
this.items.push(x)) does not trigger a render unless it's tracked. ๐ Access: Inside your own component, always use
this.template.querySelector to find elements, never the global document object.
It is technically possible to slap
@track on absolutely every variable "just in case." While it works, it is an anti-pattern. It masks the cleaner reassignment idiom that makes the actual data flow obvious to other developers reading your code.
๐ Core Q&A & Scenarios
@track, and what is the reassignment idiom that avoids using it?@track when you mutate the inside of a complex object or array. The much cleaner idiom is full reassignment using the spread operator (e.g., this.items = [...this.items, newOne]), because plain fields will automatically react to that reassignment.
Detailed breakdown:
- If you run
this.items.push(x), LWC will not trigger a UI render if the field is untracked. - If you run
this.config.label = 'y', LWC will not notice, because the top-level memory reference to the "config" object never changed. - Treating state as immutable (reassigning the whole array) forces reactivity without
@track. - This pattern is crucial because
@wiredata is strictly read-only. You must clone it before modifying it anyway. - Stating in an interview that
@trackis now the exception, rather than the default rule, immediately proves your knowledge is modern and up-to-date.
document.querySelector fail to find my component's elements, and what is the correct way to access them?A: Because of Shadow DOM encapsulation. Your component's internal HTML elements do not exist in the main document's "light DOM" tree. To reach elements inside your own component, you must use this.template.querySelector. If you need to manipulate a child component, do not try to pierce its shadow DOM—instead, call its public @api methods and let the child handle it.
A: JavaScript getters recompute every single time the component's render cycle runs. If any minor reactive change occurs on the page, the template re-renders, and your expensive 5,000-row filtering function runs again, multiplying the performance cost rapidly.
The Fixes:
- Do not do heavy derivation inside a getter. Compute the value exactly once (inside an
@wirehandler or a setter) and store it in a static variable. - Memoize the result based on input signatures so it only recalculates if the actual data changes.
- Implement virtualization or pagination so you aren't attempting to render 5,000 DOM elements at once.
- Ensure you are properly using
key=attributes inside your HTML loops to minimize the DOM diffing workload.
Mental Model: LWC re-renders reactively, but your getters are your own performance cost centers.