Diagnosing StencilJS @State/@Prop Re‑render Issues Caused by In‑place Mutations
Learn why pushing to an array or updating a nested object bound to @State or @Prop does not trigger a re‑render, and follow a step‑by‑step checklist to confirm the cause, apply immutable updates, and escalate when needed.
18 Jan 2026, 06:43 UTC

Recognizable condition
You have a Stencil component whose template reads a property decorated with @State or @Prop. The property holds an array or an object. When you call a method that mutates the value in‑place (e.g., myArray.push(item) or myObj.foo = 42), the method executes but the UI does not update. No error appears in the console.
Short cause/diagnostic table
| Symptom | Likely cause | Why it happens |
|---|---|---|
Template does not change after push or property assignment | In‑place mutation leaves the reference unchanged | Stencil’s change detection compares the previous and current reference of the decorated property; if the reference is identical, no re‑render is scheduled. |
Method runs but no update, even after @Watch on the property | Watcher only fires on reference changes | @Watch reacts to a new reference, not to deep mutations inside the same object/array. |
Ordered checks
- Confirm the property decorator
- Open the component source file (
src/components//.tsx). - Look for
@State()or@Prop()directly above the property declaration. - If the property is
@Prop, remember it is read‑only inside the component; assigning to it will cause a TypeScript error in strict mode.
- Open the component source file (
- Identify the mutation pattern
- Search the component for lines that modify the property without reassigning it, such as:
this.items.push(newItem)this.user.name = 'Ada'this.data[0].value = 10- Note whether the code creates a new array/object (e.g., spread,
Object.assign,[{...item}]) before assignment.
- Verify that the template actually binds the property
- In the component’s render method, ensure you reference the property directly, e.g.,
{this.items.map(i => <li>{i}</li>)}or{this.user.name}. - If you only use the property inside a child component via a prop, confirm the child receives the same reference.
- In the component’s render method, ensure you reference the property directly, e.g.,
- Check for build caching that may hide changes
- Run a clean build:
npm run build -- --prod(orstencil build --prodif you have the CLI installed globally). - Delete the
wwwordistfolder before rebuilding to ensure stale output is not served.
- Run a clean build:
Fixes tied to findings
Immutable update for @State
Replace in‑place mutations with assignments that create a new reference.
// ❌ In‑place push (no re‑render)
addItem(item) {
this.items.push(item);
}
// ✅ Immutable push (triggers re‑render)
addItem(item) {
this.items = [...this.items, item];
}
// ❌ In‑place object property change
updateName(name) {
this.user.name = name;
}
// ✅ Immutable object update
updateName(name) {
this.user = { ...this.user, name };
}
Run the component after making the change and observe the UI update.
Using @Watch for external @Prop changes
If the property is a @Prop supplied by a parent and you need to react when the parent replaces the whole array/object, add a watcher:
@Prop() items: any[];
@Watch('items')
itemsChanged(newVal, oldVal) {
// newVal is a different reference; you can safely recompute derived state
this.processedItems = newVal.map(i => ({ ...i, processed: true }));
}
Note: @Watch will not fire for mutations that keep the same reference; you must still rely on immutable updates from the parent side.
Lifting state to a parent
When deep nested updates are frequent, consider moving the mutable state to a parent component and passing down a new reference each time:
// Parent
render() {
return (
<child-component
items={this.parentItems} {/* new reference after mutation */}
onItemAdded={this.handleAdd}
/>
);
}
handleAdd(item) {
this.parentItems = [...this.parentItems, item];
}
The child receives a fresh reference, triggering its own render.
Escalation criteria
If after applying immutable updates the template still does not re‑render:
- Validate the build output
- Examine the generated component code (
www/build/.js) for the property decorator and any watcher metadata. Look for lines like__decorate([State()], ...). - If the decorator is missing, ensure you are not accidentally compiling a different version of the file (e.g., a stale copy in
node_modules).
- Examine the generated component code (
- Add temporary diagnostics
- Insert a
console.loginsiderender()orcomponentDidUpdate()to confirm whether the method is being called after your change. - Example:
render() { console.log('render called, items length:', this.items.length); return ... } - Insert a
- Test in a minimal reproduction
- Create a new component with only a single
@State() items: any[] = [];and a button that callsthis.items = [...this.items, {}]. - If the minimal case works, the issue lies in interactions with other properties, child components, or Stencil plugins in your original component.
- Create a new component with only a single
- Check Stencil version and changelog
- Run
npm list @stencil/coreto see the installed major version. - Review the release notes for that version (e.g., v4.x introduced async rendering changes). If you suspect a version‑specific bug, consider upgrading to the latest patch within the same major line.
- Run
Practical way to verify the fix
After applying an immutable update:
- Run the dev server:
npm start(orstencil dev). - Open the component in a browser and open the developer tools console.
- Trigger the mutation (e.g., click the “Add item” button).
- Observe the console for the temporary log you added in
render()orcomponentDidUpdate(). A new log entry indicates a re‑render occurred. - Visually confirm the UI reflects the change (new list item appears, updated text shows).
If the log appears and the UI updates, the fix is successful. If the log does not appear, revisit the checks above.
Limitations
- Immutable updates may create extra garbage‑collection pressure for large arrays; consider using libraries like
immerif performance becomes a concern. @Watchdoes not deep‑watch; nested changes still require a new reference from the owner.- The behavior described assumes Stencil’s default change detection. Custom configurations that enable experimental features (e.g.,
enableAsyncRendering: true) may alter when watchers fire, but the reference‑based rule remains.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.