@api makes a property public so the parent component can control it. @track forces a re-render when you change data inside an object or array. @wire automatically pulls data from Salesforce and refreshes whenever its inputs change. Everything else is just a plain field—and remember, plain fields in modern LWC are already reactive!
⚡ Key Points
- All plain fields are reactive: Since Spring '20, you do not need a decorator just to make a field update the UI when its value is reassigned.
@apiis for Parent-to-Child communication: It exposes a property or method publicly. The child component should never mutate its own@apiproperties.@trackis for deep reactivity: Use it only when you intend to mutate the internal properties of an array or object without completely reassigning it.@wireis a continuous subscription: It fetches data declaratively. If you prefix a parameter with a$, the wire automatically re-runs whenever that parameter changes.
๐️ Core Concept: The Big Three Decorators
To write clean Lightning Web Components (LWC), you must start with the default behavior: every plain field is reactive on reassignment. If you write this.searchKey = 'New Value', the template automatically re-renders. You do not need a decorator for this.
So, when do you actually use decorators? Let's break it down:
@api: Marks a property or method as PUBLIC. The parent component passes data into the child via markup using kebab-case (e.g.,record-idin HTML maps torecordIdin JS). Crucially, the child component must treat@apiproperties as read-only.@track: Tells the framework to observe changes inside an object or array. For example,this.filters.region = "North"will not trigger a re-render on an untracked object. However, a better and more modern practice is to simply reassign the object using the spread operator:this.filters = { ...this.filters, region: "North" }. Because reassignment is cleaner,@trackis rarely needed today.@wire: Binds a property or function directly to a data source (like a cacheable Apex method or a UI API adapter). The data returned by a wire is immutable (read-only), so you must clone it if you want to modify it.
Suppose a child component has
@api contacts = []. The Wrong Way: The child executes
this.contacts.push(newContact). This causes a bug where the UI doesn't re-render, and it illegally mutates the parent's array.The Right Way: The child fires a
CustomEvent. The parent listens for the event, adds the new contact to its own list, and the updated array naturally flows back down to the child via the @api property.
Rule:
@api means the parent owns it. @track means you mutate inside. @wire means the Salesforce platform feeds it.Gain: Three simple words that instantly tell a developer who owns a property and where its value originates.
Price:
@api properties are read-only in the child. Breaking this rule (like pushing to a public array) causes hard-to-trace bugs.Limits: Plain fields handle 90% of reactivity needs.
@wire requires a $ prefix for parameters to be dynamic.At Volume: Reassigning massive arrays on every keystroke can hurt performance. Always debounce user inputs before updating your lists.
A common mistake is thinking
@wire is a one-time data load. It is not—it is an active subscription. Furthermore, if you forget the dollar sign prefix for a dynamic parameter (e.g., passing 'recordId' instead of '$recordId'), the parameter is captured as a literal string once and will never re-fire when the record ID changes.
๐ฏ Core Q&A
Q: Walk me through @api, @track, and @wire. Which one do you actually reach for, and how often?
@api is what the parent owns, @track is for deep internal mutations, and @wire is data fed by the platform.
A: Let's look at how often they are used in modern LWC:
@api(Constantly): Used anytime a parent needs to pass data down or call a public method on a child.@wire(Often): Used for declarative, cacheable data fetching from Apex or standard Salesforce UI APIs.@track(Almost Never): Because modern LWC treats all standard fields as reactive upon reassignment, it's almost always better to create a new object/array copy rather than tracking deep mutations.
๐ Follow-ups (Scenario-Based)
Q1: A child component executes this.items.push(newRow) on an @api array. Nothing re-renders, and the parent's data quietly changes too. Explain both bugs.
A: There are two massive architectural violations here:
- The Render Bug:
push()mutates the array in place. Because the property wasn't entirely reassigned, an untracked property never detects a change, so the template fails to re-render. - The Data Flow Bug:
@apiproperties are passed by reference. By mutating it, the child has illegally reached into the parent's state. This breaks the "data flows down, actions flow up" rule of LWC. The correct pattern is for the child to dispatch an event, letting the parent handle the data update.
Q2: Your @wire needs to filter by a recordId that arrives from the Lightning record page. How do you make it react, and what happens on the first render?
A: You make the parameter reactive by prefixing it with a dollar sign: @wire(getContacts, { accountId: '$recordId' }). This tells the wire to re-fire every time this.recordId changes.
- First Render Danger: Before the page fully loads,
recordIdmight beundefined. The wire will fire immediately with thatundefinedvalue. - The Fix: You must guard your Apex method to tolerate null inputs (returning an empty list) or use a getter in your JS to prevent the wire from executing until the ID is fully populated.
@api. Actions flow up via Custom Events. Plain fields handle 90% of your UI reactivity. Save @track for complex nested objects, and rely on @wire for effortless, reactive data fetching.