---
title: "Your LWC property changed and nothing re-rendered"
url: "https://trailassembly.com/lwc-reactivity-not-updating/"
topic: "LWC and Front-End"
published: "2026-09-10T13:37:55+00:00"
updated: "2026-10-06T13:52:24+00:00"
---

# Your LWC property changed and nothing re-rendered

**The short answer:** you mutated something instead of replacing it. LWC re-renders when a field’s value is reassigned, and reassignment is not the same as changing what is inside an object or an array. `this.rows.push(x)` changes nothing the framework can see. `this.rows = [...this.rows, x]` does.

## What actually triggers a re-render

Every field on a component is [reactive](https://trailassembly.com/flow-reactive-screen-components/). When you assign a new value to it, the framework compares and re-renders the parts of the template that read it. That comparison is shallow: it looks at the field, not inside it.

So these re-render:

```apex
this.count = this.count + 1;
this.rows = [...this.rows, newRow];
this.record = { ...this.record, Name: 'New' };
```

And these do not:

```apex
this.count++;                    // this one does, actually — it reassigns
this.rows.push(newRow);          // mutation, no reassignment
this.rows[0].Name = 'New';       // mutation, deeper
this.record.Name = 'New';        // mutation
this.rows.sort();                // mutation, and it sorts in place
```

The pattern is one line long: **replace, do not modify**. Build a new array or object and assign it.

## @track, and why you probably do not need it

Before Spring ’20 you needed `@track` to make fields reactive at all. Now every field is reactive, and `@track` only adds one thing: it makes changes *inside* an object or array reactive too.

Reaching for it is usually a sign the mutation habit is still there. Two reasons to prefer replacement anyway: it makes the change obvious in code review, and deep tracking on a large structure costs more than one array copy.

Where `@track` genuinely helps is a big nested structure you are updating in many places and would rather not clone. Rare, and worth a comment when you do it.

## Getters run on every render, which cuts both ways

A getter is recomputed whenever the template re-renders, so derived values stay correct without any tracking:

```apex
get openRows() {
    return this.rows.filter((r) => !r.closed);
}
```

That is the right way to express derived state. It is also a trap for expensive work: a getter that sorts a thousand rows runs again on every unrelated re-render. If it is expensive, compute it when the source changes and store the result.

And a getter reading something that is never reassigned will never look different, however often it runs. Reactivity comes from the assignment, not from the getter.

## Lists: the key attribute decides what gets reused

In a `for:each` or `iterator`, the `key` tells the framework which DOM node belongs to which item. Get it wrong and rows appear not to update — the data changed, but the framework reused a node it believed was the same item.

```apex
<template for:each={rows} for:item="row">
    <li key={row.Id}>{row.Name}</li>
</template>
```

Two rules that catch most of it. The key must be unique among siblings and stable across renders, so a record Id is right and an array index is wrong — an index means every row shifts identity when you insert at the top. And the key must be a primitive: an object will not work.

The symptom is distinctive. Delete the middle row and the wrong one seems to vanish, or an input in a list keeps its value while the label above it changes. That is not a reactivity failure. It is the framework reusing exactly what you told it to.

## The four causes, in the order I check them

1. **A mutation.** `push`, `splice`, `sort`, direct property assignment. Replace instead.

2. **A missing dollar in a wire.** `{ recordId: '$recordId' }` re-runs when it changes; `{ recordId: this.recordId }` is read once and never again.

3. **Assigning to a getter’s source that nothing renders.** If no template expression reads it, nothing re-renders. Correctly.

4. **Async work resolving after the component is gone,** or resolving into a stale copy captured in a closure.

## Debugging it in two minutes

Add a `renderedCallback()` with a log. If it fires and the DOM is still wrong, your data changed and the template is reading something else. Usually a differently-named property. If it does not fire, the framework saw no change, and you are looking at a mutation.

That one test splits the problem in half and takes ten seconds, and it is the same first move as the [stale-data-after-save](https://trailassembly.com/lwc-stale-data-after-save/) diagnosis, which is faster than reading the component again hoping to spot it.

## Frequently asked

**Is spreading a big array slow?** A shallow copy of a few hundred rows is not your performance problem. Rendering a few hundred rows might be.

**Does @track still work?** Yes, and it is still the documented way to get deep reactivity. It is just no longer required for basic reactivity.

**Why does my child component not update?** Same rule one level down: a public property receives a new value only when the parent assigns a new value. Mutating the object the parent passed changes nothing.

**Do I need to worry about this in Jest tests?** Yes, and helpfully so. A test that asserts on the DOM after a mutation will fail for exactly the reason production does, which is one of the better arguments for writing them.

---

Source: https://trailassembly.com/lwc-reactivity-not-updating/
Trail Assembly — independent Salesforce write-ups.
