Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

Short answer: every field on a modern LWC class is already reactive, so @track is only needed when you mutate the inside of an object or array rather than reassigning it. Derived values belong in getters, not in extra fields that you keep in sync by hand. A getter runs during render, reads whatever reactive fields it touches, and re-runs whenever one of those fields is reassigned. That is the whole model.

What actually triggers a re-render

Reassigning a field that the template reads. Nothing else.

export default class OrderSummary extends LightningElement {
    orders = [];
    filter = 'open';

    get visibleOrders() {
        return this.orders.filter((o) => o.status === this.filter);
    }
}

Set this.filter = 'closed' and the component re-renders, because the template reads visibleOrders and that getter read filter. Set this.orders = [...newRows] and the same thing happens.

Now the failure case. this.orders.push(row) changes the contents of the array without changing the reference, and the framework sees no assignment. The screen does not update. People then reach for @track, which does fix it, but the cleaner fix is to stop mutating:

// does not render
this.orders.push(newRow);

// renders
this.orders = [...this.orders, newRow];

What @track still does

Since the reactivity change in Spring ’20, @track has exactly one job: make a field deeply reactive, so that changing a nested property or an array element triggers a render.

@track draft = { name: '', amount: 0 };

handleNameChange(event) {
    this.draft.name = event.target.value; // renders, because of @track
}

Without the decorator you would write this.draft = { ...this.draft, name: event.target.value }. Both work. The tracked version reads better for a form object with a dozen fields. The spread version is easier to reason about when several handlers touch the same object. Pick one and be consistent within a component.

@track on a primitive does nothing at all. If you see it on a string or a boolean, it is left over from an older codebase.

Why a getter is usually right

The alternative to a getter is a second field that you recalculate in every handler that could have changed its inputs. That field is now a cache, and caches go stale. A getter cannot go stale, because it is computed at the moment it is read.

get hasNoResults() {
    return this.searchTerm && this.results.length === 0;
}

get buttonLabel() {
    return this.isEditing ? 'Save' : 'Edit';
}

get rowClass() {
    return this.isSelected ? 'slds-box slds-theme_shade' : 'slds-box';
}

Templates cannot do arithmetic, cannot compare values, and cannot call functions with arguments. Anything more complex than reading a property has to be a getter, so you will write them regardless. Use them for all derived state and the question of when to recalculate disappears.

Where getters go wrong

A getter runs on every render, and it may run several times in one pass if several bindings reference it. Three rules follow.

  • No side effects. A getter that assigns to a reactive field will re-render, which calls the getter again.
  • No Apex, no fetch, no anything asynchronous. A getter must return a value synchronously.
  • Keep it cheap. Sorting ten thousand rows inside a getter that fires on every keystroke is a real performance problem, and the fix is to sort when the data arrives and store the result.

There is one subtle trap with objects. A getter that builds and returns a new object or array every time produces a new reference on every render, so a child component receiving it as an @api property sees a changed value each pass and re-renders too. For a small child this is invisible. For a datatable with a thousand rows it is not.

Setters for public properties

When you need to react to a value arriving from a parent, pair an @api setter with a private field.

@api
get contactId() {
    return this._contactId;
}
set contactId(value) {
    this._contactId = value;
    this.loadContact();
}

The decorator goes on the getter, not the setter, and both must exist. This is also how you handle a component that stays mounted while the record behind it changes, which happens on navigation between records more often than people realise.

Frequently asked

Do I still need @track anywhere? Only for deep mutation of an object or array field. If you always reassign, you never need it.

Can a getter take a parameter? No. If you need per-row values in a loop, compute them into the array before rendering and read them as properties in the template.

Why does my child component re-render when nothing changed? Probably a getter handing it a freshly built object each pass. Cache the result in a field when the inputs change.

Is a getter slower than a field? Marginally, and it almost never matters. Measure before you trade correctness for it.