Building Reactive Stencil Components with Props and Events
Learn how to implement unidirectional data flow in StencilJS using @Prop, @State, and @Event to create scalable, framework-agnostic web components.
04 Sept 2025, 08:01 UTC

The Challenge of Component Communication
When building a design system with StencilJS, the primary technical hurdle is managing the data flow between a parent application and a reusable child component. Without a strict pattern for data input (Props) and state notification (Events), components become tightly coupled, making them difficult to maintain or port across different frameworks like React, Angular, or Vue.
The solution is the Unidirectional Data Flow pattern: data flows down into the component via @Prop() and flows up to the parent via @Event(). This ensures the component remains a "pure" UI element that doesn't unexpectedly mutate global state.
Prerequisites
- Node.js 18 or higher.
- A Stencil project initialized via
npm init stencil. - Basic familiarity with TypeScript decorators.
Implementing a Reactive Component
To demonstrate this, we will build a CustomCounter component. This component will accept a starting value, track an internal count, and notify the parent when the value changes.
1. Define the Component Structure
Create a new file in src/components/custom-counter.tsx. Start by importing the necessary decorators from @stencil/core.
import { Component, Prop, State, Event, EventEmitter, h } from '@stencil/core';
@Component({
tag: 'custom-counter',
styleUrl: 'custom-counter.css',
shadow: true,
})
export class CustomCounter {
// Data flowing IN
@Prop() startValue: number = 0;
// Internal mutable state
@State() count: number;
// Data flowing OUT
@Event() countChanged: EventEmitter<number>;
componentWillLoad() {
this.count = this.startValue;
}
increment = () => {
this.count++;
this.countChanged.emit(this.count);
}
render() {
return (
<div>
<p>Current Count: {this.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
Technical Breakdown of Decorators
| Decorator | Purpose | Reactivity |
|---|---|---|
@Prop() |
Public API for the component. Set via HTML attributes. | Triggers render() when the attribute changes. |
@State() |
Private internal data. Not visible to the parent. | Triggers render() when modified internally. |
@Event() |
Custom DOM events to notify the parent of changes. | N/A (Communication mechanism). |
Verification and Testing
To verify the implementation, you must compile the TypeScript code into web components. Run the following command in your terminal from the project root:
npm run build
Expected Checks:
- Build Log: Ensure there are no TypeScript type-mismatch errors regarding the
EventEmitter. - DOM Inspection: Open the generated
index.html. Add the component:<custom-counter start-value="10"></custom-counter>. - Event Listener: In the browser console, add a listener to verify the event:
document.querySelector('custom-counter').addEventListener('countChanged', (e) => console.log('New count:', e.detail));
Common Pitfalls and Limitations
- Attribute Naming: Stencil automatically converts camelCase Props (e.g.,
startValue) to kebab-case attributes (e.g.,start-value) in HTML. UsingstartValue="10"in HTML may result in the prop not being set. - Complex Types:
@Prop()works best with primitives (strings, numbers, booleans). If you pass an object or array, Stencil does not deeply observe changes within that object; you must replace the entire object reference to trigger a re-render. - State Initialization: Do not initialize
@Statedirectly from a@Propin the class property declaration. UsecomponentWillLoad()to ensure the prop has been received from the DOM before assigning it to the state.
Rollback and Recovery
If the component fails to render or the state does not update, follow these recovery steps:
- Clear Build Artifacts: Delete the
dist/andwww/folders to remove stale compiled JS. - Verify Version: Check
package.jsonto ensure@stencil/coreis above version 1.0 to ensure proper type inference for Props. - Rebuild: Run
npm run buildagain.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.