Reactivity is what makes the template update when state changes. In LWC, the framework wraps your component’s fields in Proxies that observe writes — when you assign to a tracked field, the framework schedules a re-render on the next microtask.
What’s reactive by default
Since LWC API version 49 (Winter ‘21), the following are reactive without any decorator:
- Primitive fields — strings, numbers, booleans
- Object and array fields — but only on reassignment of the whole value, not on internal mutation
@apiproperties — public properties are always reactive@wireresults — thedata/errorpayload re-renders the template on change
import { LightningElement } from 'lwc';
export default class Counter extends LightningElement {
count = 0;
items = [];
increment() {
this.count++; // reactive — re-renders
this.items = [...this.items, 'new']; // reactive — new array reference
}
}
What is not reactive without help
Mutating the insides of an object or array doesn’t trigger a re-render:
addItem(name) {
this.items.push(name); // BAD — same array reference, no re-render
this.config.label = 'updated'; // BAD — same object reference, no re-render
}
You have two fixes:
Fix 1 — Immutable updates (preferred). Replace the reference instead of mutating it:
this.items = [...this.items, name];
this.config = { ...this.config, label: 'updated' };
Fix 2 — @track. Add the decorator to opt that field into deep observation:
import { LightningElement, track } from 'lwc';
export default class Settings extends LightningElement {
@track config = { label: 'old', enabled: false };
update() {
this.config.label = 'new'; // re-renders now because of @track
}
}
Why immutable updates are the modern default
When LWC launched in 2019, @track was required for every field. That changed in Winter ‘21 — the team made shallow reactivity the default and demoted @track to a deep-mutation escape hatch. Most production teams now write LWC without ever importing track:
- Immutable updates compose well with
map,filter, spread, and other array methods. - They make change tracking explicit and easier to reason about.
- They avoid the Proxy overhead
@trackintroduces on hot paths.
The cases where @track still pulls its weight: forms with deeply nested config objects you genuinely want to mutate in place, and legacy code where the immutable rewrite is more risk than it’s worth.
How rendering is scheduled
When you assign to a reactive field, LWC doesn’t re-render synchronously. Instead it schedules the render on the next microtask, batching all changes in the current tick:
handleClick() {
this.a = 1; // schedules render
this.b = 2; // already scheduled — no extra work
this.c = 3; // still one render
}
This is why you should never await after a state change expecting the DOM to be updated synchronously — the render hasn’t happened yet. If you need the new DOM, wait one microtask:
async handleClick() {
this.showModal = true;
await Promise.resolve(); // let the render tick happen
this.refs.modal.focus();
}
Getters are reactive too
Computed values via getters automatically participate in reactivity — they re-evaluate whenever their dependencies change:
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
When firstName or lastName changes, the template re-renders and fullName is recalculated on the new render pass. There’s no useMemo-style memoisation — if you need caching, store the value in a field manually.
Interview-worthy gotchas
Object.assign(this.config, {...})doesn’t trigger a re-render unlessconfigis@track’d. You replaced the contents but not the reference.- Maps and Sets are not reactive at all. Wrap them in a class or, more commonly, convert to a plain object/array on the surface.
- Reading a reactive field inside an async callback sees the latest value, not the value at the time the callback was scheduled. Capture it in a local if that matters.
Verified against: LWC Developer Guide — Reactivity. Last reviewed 2026-05-17 for Spring ‘26 release.