Managing Component State in StencilJS: @Prop vs @State
Learn how to effectively use @Prop and @State in StencilJS to manage component re-renders and avoid common pitfalls with object mutation in Web Components.
09 Jul 2025, 03:00 UTC

The Re-render Trigger Problem
When building Web Components with StencilJS, a common point of friction is deciding where a piece of data lives. If you place a value in the wrong decorator, you either face a component that refuses to update when data changes or a component that re-renders far more often than necessary, killing performance in complex UIs.
The core takeaway is simple: @Prop is for external configuration (API), and @State is for internal UI logic. Misusing these leads to "silent failures" where the Virtual DOM doesn't detect a change, leaving your UI out of sync with your data.
Defining the Public API with @Prop
The @Prop() decorator defines the public interface of your component. These are the attributes that a developer using your component will set in HTML or via JavaScript. When a parent component changes a prop value, Stencil automatically triggers a re-render of the child component.
Crucially, props should be treated as read-only within the child component. Attempting to mutate a prop directly is a technical anti-pattern; if a child needs to change a value passed from a parent, it should emit a custom event to request that change from the source of truth.
Handling Internal Logic with @State
The @State() decorator is for data that is private to the component. This includes things like toggle switches, loading indicators, or temporary form input values. Unlike props, state is not visible in the HTML attribute list and cannot be set by a parent.
Stencil uses a Virtual DOM to optimize these updates. When a @State variable changes, Stencil calculates the difference (diffing) between the current and next state and updates only the specific DOM nodes that changed, rather than re-rendering the entire component tree.
Worked Example: A Controlled Counter
Consider a component that displays a label provided by the parent but maintains its own internal click count. This demonstrates the separation of external configuration and internal state.
import { Component, Prop, State, h } from '@stencil/core';
@Component({
tag: 'my-counter',
styleUrl: 'my-counter.css',
shadow: true,
})
export class MyCounter {
// Public API: Set via <my-counter label="Clicks">
@Prop() label: string = 'Count';
// Internal State: Managed only by this component
@State() count: number = 0;
increment = () => {
this.count++;
};
render() {
return (
<div>
<p>{this.label}: {this.count}</p>
<button onClick={this.increment}>Add
</div>
);
}
}
Verification Steps
- Run the component in a development server.
- Test State: Click the button. You should see the count increment. In the browser's Elements tab, observe that only the text node containing the number updates.
- Test Prop: Use the browser console to change the attribute:
document.querySelector('my-counter').setAttribute('label', 'Total'). The label should update immediately without resetting the count.
The Mutation Trap: Objects and Arrays
One of the most frequent pitfalls in StencilJS is mutating objects or arrays assigned to @State. Because Stencil checks for reference changes to trigger a re-render, simply pushing an item into an array will not update the UI.
The Wrong Way:
this.items.push(newItem); // The reference to the array remains the same; no render.
The Right Way:
this.items = [...this.items, newItem]; // Creates a new array reference; triggers render.
Decision Matrix
| Feature | @Prop | @State |
|---|---|---|
| Visibility | Public (HTML Attribute) | Private (Internal) |
| Source of Truth | Parent Component | Self |
| Mutation | Read-only (Emit event to change) | Mutable (via new reference) |
| Render Trigger | External change | Internal change |
Closing Summary
To keep your StencilJS components scalable, strictly enforce the boundary between @Prop and @State. Use props for the "what" (configuration) and state for the "how" (behavior). When working with complex data, always replace references rather than mutating them to ensure the Virtual DOM stays in sync with your application logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.