Managing Component Data Flow with StencilJS @Prop and @State
Learn how to distinguish between @Prop and @State in StencilJS to create clean public APIs and efficient internal state management, including the use of mutable props.
24 Feb 2026, 07:11 UTC

The Problem: Distinguishing Public API from Internal State
In StencilJS, a common engineering mistake is using @Prop for every piece of data that triggers a re-render. This creates a "leaky" component API where internal implementation details are exposed as public attributes, making the component harder to maintain and potentially confusing for the developers consuming your custom element.
The takeaway: Use @Prop exclusively for data passed from a parent or the DOM (the public API). Use @State for data that is managed entirely within the component (internal state). If a public property must be modified internally, use the { mutable: true } option.
How @Prop and @State Control Rendering
StencilJS uses a virtual DOM. When a variable decorated with @Prop or @State changes, Stencil schedules a call to the render() method to update the UI. However, they behave differently regarding the DOM and data flow.
- @Prop: Defines a property that is reflected as an HTML attribute. It allows external entities to pass data into the component. By default, props are intended to be immutable from within the component to maintain a one-way data flow.
- @State: Defines internal data. It is not reflected as an HTML attribute and cannot be set by a parent component. It is purely for the component's internal logic.
Practical Implementation: A Search Input Component
Consider a search component that receives a placeholder from the parent but manages the currentQuery internally.
import { Component, Prop, State, h } from '@stencil/core';
@Component({
tag: 'my-search-input',
styleUrl: 'my-search-input.css',
shadow: true,
})
export class MySearchInput {
// Public API: Parent can set this via <my-search-input placeholder="Search...">
@Prop() placeholder: string = 'Enter search term...';
// Internal State: Only the component knows the current typing value
@State() currentQuery: string = '';
// Mutable Prop: Allows the component to change a public prop internally
@Prop({ mutable: true }) status: string = 'idle';
handleInput(event: Event) {
const input = event.target as HTMLInputElement;
this.currentQuery = input.value;
this.status = 'typing'; // Works because status is { mutable: true }
}
render() {
return (
<div>
<input
type="text"
placeholder={this.placeholder}
onInput={(ev) => this.handleInput(ev)}
/>
<p>Current Query: {this.currentQuery}</p>
<p>Status: {this.status}</p>
</div>
);
}
}
Critical Constraints and Common Pitfalls
Attribute String Limitation
HTML attributes are always strings. If you define a @Prop() as an object or array, you cannot pass that data via the HTML tag (e.g., <my-component data='{ "id": 1 }'> will not work as expected). Complex data types must be passed via JavaScript properties after the element is instantiated in the DOM.
The Mutation Trap
If you attempt to update a @Prop that is NOT marked as { mutable: true } from within the component, Stencil will not trigger a re-render. This often leads to "silent failures" where the variable value changes in memory, but the UI remains static.
Reflection Overhead
By default, Stencil reflects property changes back to the DOM attributes. While useful for CSS styling (e.g., [status="active"]), reflecting large strings or frequent updates can cause performance degradation due to constant DOM manipulation.
Verification and Diagnostics
To verify your data flow is configured correctly, perform these checks in the browser console:
- Public API Check: Select the element in the Elements tab. Change a
@Propvalue via JavaScript:document.querySelector('my-search-input').placeholder = 'New Text';. The UI should update immediately. - Internal State Check: Attempt to change a
@Statevariable from the console:document.querySelector('my-search-input').currentQuery = 'Hack';. This should either fail or not trigger a re-render, as@Stateis private. - Attribute Reflection: Observe the HTML tag in the inspector. When
this.statuschanges in the code, thestatusattribute on the<my-search-input>tag should update in real-time.
Rollback and State Reset
Because @Prop values are driven by the parent, the only way to "roll back" a prop to its original state is for the parent component to re-assign the previous value to the attribute or property. For @State, you must manually implement a reset method within the component to clear the internal variables.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.