Managing Component State in StencilJS: Choosing Between @Prop and @State
Learn when to use @Prop vs @State in StencilJS to avoid unidirectional data flow violations and unnecessary re-renders in your web components.
18 Jan 2026, 14:04 UTC

The State Management Dilemma
In StencilJS, the primary challenge in component design is deciding where a piece of data lives. Choosing the wrong decorator for a variable leads to either a "frozen" UI that won't update or a "leaky" component that violates unidirectional data flow—where data moves from parent to child, but not vice versa.
The core takeaway is that @Prop() is for external configuration (what the parent tells the component) and @State() is for internal behavior (what the component tracks itself).
Comparing @Prop and @State
| Feature | @Prop() | @State() |
|---|---|---|
| Visibility | Public API (HTML attributes) | Private/Internal |
| Source of Truth | Parent Component / HTML | The Component itself |
| Triggers Render | Yes, when external value changes | Yes, when internal value changes |
| Mutability | Read-only (by convention) | Mutable |
Trade-offs and Engineering Constraints
The Risk of Prop Mutation
Attempting to mutate a @Prop() directly inside a component is a common anti-pattern. While JavaScript may allow the assignment, Stencil's reactivity system is designed for unidirectional flow. If a child component changes its own prop, the parent remains unaware of the change, creating a synchronization gap where the DOM may reflect one value while the parent's state reflects another.
The Overhead of @State
Every change to a @State() variable triggers a re-render of the component's Virtual DOM. Using @State() for values that do not impact the render() method—such as a timer ID or a temporary calculation variable—introduces unnecessary performance overhead.
The Initialization Pattern
Often, a component needs a value from a parent but also needs to be able to modify that value locally (e.g., a text input that starts with a default value). In this scenario, the best practice is to use a @Prop() to initialize a @State() variable during the componentWillLoad() lifecycle hook.
Implementation: The Controlled Input Pattern
The following example demonstrates a search component that accepts an initial value from a parent but manages its own typing state 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: The parent sets the starting value
@Prop() initialValue: string = '';
// Internal State: Tracks the current user input
@State() currentSearch: string = '';
componentWillLoad() {
// Initialize internal state from the prop
this.currentSearch = this.initialValue;
}
handleInput(event: Event) {
const input = event.target as HTMLInputElement;
// Mutate state, not the prop
this.currentSearch = input.value;
}
render() {
return (
this.handleInput(ev)}
/>
Searching for: {this.currentSearch}
);
}
}
Validation and Verification
To verify this implementation is working correctly, perform these three checks in a browser environment:
- External Update: Use the browser inspector to change the
initial-valueattribute on the<my-search-input>element. If the component is correctly using@Prop(), the component should react to the change (provided you have acomponentWillUpdateor similar listener). - Internal Update: Type into the input field. The "Searching for:" text should update instantly. This confirms
@State()is triggering the render. - Unidirectional Check: Verify that the
initialValueprop remains unchanged even after you type in the box. This ensures you are not mutating the public API.
Rollback Strategy
If you discover a component is performing too many renders, remove the @State() decorator from variables that are not referenced in the render() function. Convert them to standard class properties (e.g., private myValue: string;). This stops the reactivity engine from tracking that specific variable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.