@qery/reactive-component v0.3.0
Reactive Component
Reactive Component is a lightweight library for building modern web components with native standards. It extends the Custom Elements API by adding a signal-based reactive system for declarative DOM manipulation and precise state management. Using simple directives like $state and $bind-*, you can easily bind the component state to the DOM, keeping your UI in sync with changes.
This HTML-first approach lets you work directly with your existing markup—there is no need for virtual DOM diffing, heavy templating, or build steps. Computed properties and automatic dependency tracking handle updates, so you can focus on your app’s logic.
In summary, Reactive Component offers reactive state management and declarative data binding in a simple, standards-compliant way—without the complexity of larger frameworks.
Features
- Reactive: Automatically update the DOM when the state changes.
- Computed: Instantly refresh derived values.
- Declarative Binding: Keep UI and data in sync with minimal code.
- Zero Build: Use plain HTML and Custom Elements, no bundlers needed.
- Progressive Enhancement: Boost SEO, speed up initial loads, and simplify maintenance.
- High Performance: Direct DOM updates in a ~3.5KB gzipped package.
- TypeScript: Benefit from type safety and smarter tooling.
- Framework Agnostic: Easily integrate with other libraries or legacy systems.
- Context API: Share state between components in a clean, React-like way.
Credits
Hawk Ticehurst's work on Stellar and his article 'Declarative Signals' inspired this library.
Installation
# Using pnpm (recommended)
pnpm install @qery/reactive-componentOr
# Using npm
npm install @qery/reactive-componentQuick Start
Let's start with a simple counter component that demonstrates several core reactive features:
Key Features Demonstrated:
Automatic State Management
- The
countproperty is automatically tracked and managed - No explicit state initialization is required
- Changes trigger efficient DOM updates
- The
Method Auto-binding
- Component methods are automatically bound to the instance
- Clean event handler syntax
Two-way Data Binding
- The
$statedirective creates bidirectional binding - DOM updates when state changes
- State updates when DOM changes
- The
Here's the complete example:
class BasicCounter extends ReactiveComponent {
// Methods are automatically bound to the component instance
increment() {
this.count++; // Direct property access thanks to proxy handlers
}
decrement() {
this.count--;
}
}
customElements.define("basic-counter", BasicCounter);<basic-counter>
<!-- $state directive creates a two-way binding -->
<p>Count: <span $state="count">0</span></p>
<!-- onclick attribute automatically binds to component methods -->
<button onclick="decrement" class="bg-blue-500 text-white px-4 py-2 rounded">Decrement</button>
<button onclick="increment" class="bg-blue-500 text-white px-4 py-2 rounded">Increment</button>
</basic-counter>Implementation Details:
State Declaration
- Type inference automatically handles number type
- Initial value of 0 is set and reflected in DOM
Method Implementation
increment()anddecrement()directly modify state- Proxy handlers convert property access to state updates
- Changes automatically trigger view updates
Template Structure
$state="count"creates two-way binding for the counteronclickhandlers map directly to component methods- Tailwind classes provide styling without extra CSS
Core Concepts
- State Management - Reactive state that automatically updates the UI
- Computed Properties - Derived values that update when dependencies change
- Context API - Share state between components without prop drilling
State Management
The component uses a sophisticated signal-based reactive system for efficient state management that provides several powerful features:
Declarative State Initialization
- States are initialized using
setState()with automatic type inference - Values can be primitives, objects, or complex data structures
- State changes trigger efficient, granular re-renders
- States are initialized using
Computed Properties with Auto-Tracking
- Derived values update automatically when dependencies change
- Smart caching prevents unnecessary recalculations
- Dependencies are tracked without explicit declarations
Two-Way Data Binding
- State changes automatically sync with the DOM
- DOM events update state seamlessly
- No manual DOM manipulation is needed
Here's a practical example showing these features in action:
class InputEcho extends ReactiveComponent {
constructor() {
super();
// Initialize reactive state with empty string
this.setState("text", "");
// Create a computed property that transforms text to uppercase
// Updates automatically when text changes
this.compute("uppercase", ["text"], (c) => c.toUpperCase());
}
}
customElements.define("input-echo", InputEcho);<input-echo>
<!-- Two-way binding syncs input value with text state -->
<input type="text" $bind-value="text" class="border p-2 w-full mb-2" placeholder="Type something..." />
<!-- Displays computed uppercase value -->
<!-- Updates automatically when text changes -->
<p>You typed: <span $bind-text="uppercase" /></p>
</input-echo>Key Features Demonstrated:
- Automatic state synchronization between input and component
- Computed properties that transform state (
uppercase) - Efficient updates that only re-render affected parts
- Clean, declarative syntax for complex reactivity
Computed Properties
Computed properties are a powerful feature that enables you to create derived state values that automatically update based on changes to their dependencies. This reactive computation system provides several key benefits:
Automatic Dependency Tracking
- The system intelligently tracks dependencies between state values
- Only recomputes when dependent values change
- Eliminates unnecessary calculations and improves performance
Smart Caching
- Computed values are cached until dependencies change
- Prevents recalculating the same value multiple times
- Optimizes memory usage and computation time
Declarative Data Flow
- Define transformations as pure functions
- Dependencies are automatically managed
- Results update seamlessly when source data changes
Here's a practical example of computed properties in action with a temperature converter:
class TemperatureConverter extends ReactiveComponent {
constructor() {
super();
// Initialize base temperature in Celsius
this.setState("celsius", 20);
// Compute Fahrenheit from Celsius
// Updates automatically when celsius changes
this.compute("fahrenheit", ["celsius"], (c) => {
// Standard C to F conversion formula
return (c * 9) / 5 + 32;
});
}
}
customElements.define("temperature-converter", TemperatureConverter);<temperature-converter>
<div class="space-y-2">
<!-- Base temperature with two-way binding -->
<p>Celsius: <span $state="celsius">20</span>°C</p>
<!-- Computed temperatures update automatically -->
<p>Fahrenheit: <span $bind-text="fahrenheit" />°F</p>
<!-- Interactive slider updates celsius state -->
<input type="range" min="0" max="40" $bind-value="celsius" class="w-full" />
</div>
</temperature-converter>Key Features Demonstrated:
- Automatic updates when source data changes
- Multiple computed properties from a single source
- Built-in caching and performance optimization
- Clean separation of computation logic
- Declarative template bindings
Advanced Features
Element References
Element references provide direct, type-safe access to DOM elements in your components. This feature enables efficient manipulation of DOM elements while maintaining reactivity and encapsulation.
Type-Safe Element Access
- Direct access to DOM elements through the
refsobject - Automatic type inference for element properties
- Compile-time checking for element existence
- Direct access to DOM elements through the
Ref Registration
- Elements marked with
$refattribute are automatically registered - References are available immediately after component mounting
- Clean separation between template and logic
- Elements marked with
Reactive Integration
- Refs work seamlessly with reactive state
- Changes through refs trigger appropriate updates
- Maintains the component's reactive nature
Here's a practical example demonstrating element references:
class RefDemo extends ReactiveComponent {
constructor() {
super();
// Initialize state with default values
this.setState("outputText", "Initial Text");
this.setState("outputColor", "black");
}
updateText() {
this.refs.output.textContent =
this.refs.output.textContent === "Initial Text"
? `Updated Text Content (${this.clickCount} clicks)`
: "Initial Text";
}
updateColor() {
// Toggle text color between black and orange
this.outputColor = this.outputColor === "black" ? "orange" : "black";
this.refs.output.style.color = this.outputColor;
}
}
customElements.define("ref-demo", RefDemo);<ref-demo>
<div class="space-y-4">
<p>Click buttons to update referenced element:</p>
<!-- Element referenced using $ref attribute -->
<p $ref="output" class="text-xl font-bold text-center p-4 border rounded">Initial Text</p>
<div class="flex justify-center space-x-4">
<!-- Event handlers trigger ref-based updates -->
<button onClick="updateText" class="bg-purple-500 text-white px-4 py-2 rounded">Update Text</button>
<button onClick="updateColor" class="bg-pink-500 text-white px-4 py-2 rounded">Change Color</button>
</div>
</div>
</ref-demo>Key Features Demonstrated:
- Simple element referencing with
$refattribute - Direct access to DOM methods and properties
- Integration with component state management
- Clean separation of concerns between template and logic
- Type-safe element manipulation
- Automatic reference management
- Event handling with referenced elements
JSON State Management
The JSON State Management feature provides sophisticated handling of complex data structures with automatic serialization and reactive updates. Here's a detailed breakdown of its capabilities:
Automated JSON Serialization
- Automatic conversion between JS objects and JSON
- Pretty-printing with proper indentation
- Type-safe serialization of complex nested structures
Deep Reactivity
- Changes to nested properties trigger updates
- Computed properties track deep dependencies
- Metadata automatically updates on state changes
Form Integration
- Two-way binding with form inputs
- Real-time validation and feedback
- Automatic type coercion for form fields
Here's a practical example demonstrating JSON state management:
class JsonStateManager extends ReactiveComponent {
constructor() {
super();
// Initialize form field states
this.setState("name", "Paco Doe");
this.setState("age", 30);
this.setState("bio", "");
// Create computed JSON representation with metadata
// Updates automatically when any dependency changes
this.compute("json", ["name", "age", "bio"], (name, age, bio, lastUpdated) =>
JSON.stringify(
{
name,
age,
bio,
},
null,
2,
),
);
}
}
customElements.define("json-state-management", JsonStateManager);<json-state-management>
<div class="space-y-4">
<div>
<!-- Two-way binding for name field -->
<label for="name" class="block mb-1">Name:</label>
<input id="name" type="text" $bind-value="name" class="border p-2 w-full" />
</div>
<div>
<!-- Numeric input with automatic type coercion -->
<label for="age" class="block mb-1">Age:</label>
<input id="age" type="number" $bind-value="age" class="border p-2 w-full" />
</div>
<div>
<!-- Multi-line text input with two-way binding -->
<label for="bio" class="block mb-1">Bio:</label>
<textarea id="bio" $bind-value="bio" class="border p-2 w-full h-24"></textarea>
</div>
<div>
<!-- Live JSON preview with automatic updates -->
<p>JSON Output:</p>
<pre class="bg-gray-100 p-2 rounded mt-1">
<code $bind-text="json"></code>
</pre>
</div>
</div>
</json-state-management>Key Features Demonstrated:
- Deep reactive state management with nested objects
- Automatic JSON serialization and formatting
- Real-time form input synchronization
- Computed metadata updates
- Type-safe state handling
- Clean separation of data and presentation
- Automatic dependency tracking
Context API
Context provides a way to share values between components without having to explicitly pass a prop through every level of the component tree. This feature is particularly useful for sharing global state such as themes, user data, or application configuration.
Creating Context
- Define shared state using the
createContextfunction - Provide a state key for storage and an optional debug name
- Context is identified by a unique symbol to prevent collisions
- Define shared state using the
Exposing Context
- Provider components expose context using
exposeContext(context) - State changes in provider components automatically update consumers
- Multiple contexts can be exposed from a single component
- Provider components expose context using
Consuming Context
- Child components consume context using
consumeContext(context) - Consumed context is automatically synchronized with provider updates
- Components can consume multiple contexts from different providers
- Child components consume context using
Here's an example of a theme context system:
// Define context and provider component
const themeContext = createContext('theme');
class ThemeProvider extends ReactiveComponent {
constructor() {
super();
// Initialize state
this.setState('theme', {
mode: 'light',
background: 'bg-slate-200',
text: 'text-slate-900'
});
// Expose the theme context
this.exposeContext(themeContext);
}
toggleTheme() {
const currentTheme = this.getState('theme');
// Toggle between light and dark mode
this.setState('theme', currentTheme.mode === 'light'
? { mode: 'dark', background: 'bg-slate-900', text: 'text-slate-50' }
: { mode: 'light', background: 'bg-slate-200', text: 'text-slate-900' }
);
}
}
customElements.define('theme-provider', ThemeProvider);
// Consumer component
class ThemeConsumer extends ReactiveComponent {
constructor() {
super();
// Consume the theme context
this.consumeContext(themeContext);
// Create computed properties based on the theme
this.compute('themeMode', [themeContext.state], (theme) => `ThemeMode: ${theme.mode}`);
}
connectedCallback() {
super.connectedCallback();
// React to theme changes
this.effect(() => {
const theme = this.getState('theme');
this.classList.add(theme.background, theme.text);
this.refs.themeInfo.textContent = `Current Theme: ${theme.mode}`;
});
}
}
customElements.define('theme-consumer', ThemeConsumer);<theme-provider>
<button type="button" onClick="toggleTheme">Toggle Theme</button>
<theme-consumer>
<p $bind-text="themeMode"></p>
<p $ref="themeInfo"></p>
</theme-consumer>
</theme-provider>Key Features Demonstrated:
- Clean provider/consumer pattern for shared state
- Automatic propagation of state changes
- Type-safe context consumption
- Computed properties based on context values
- Nested component communication without prop drilling
- Reactive UI updates when context changes
Form Handling
Form handling in ReactiveComponent provides sophisticated validation, state management, and real-time feedback capabilities. Here's a detailed breakdown of its features:
Reactive Form State Management
- Automatic two-way data binding for form inputs
- Real-time validation and error handling
- Dynamic enable/disable functionality
- Status tracking and feedback
Smart Validation System
- Built-in validation rules and custom validators
- Real-time validation feedback
- Computed validation states
- Error message management
Accessibility Integration
- Keyboard navigation support
- Screen reader-friendly status messages
- Focus management
Here's a practical example demonstrating form-handling capabilities:
class FormDemo extends ReactiveComponent {
constructor() {
super();
// Initialize form state
this.setState("isEnabled", document.getElementById("enabled")?.checked ?? false);
this.setState("inputText", "");
// Compute disabled state from isEnabled
// Updates automatically when enabled state changes
this.compute("isDisabled", ["isEnabled"], (enabled) => !enabled);
// Compute status message with validation
// Re-computes when either input text or enabled state changes
this.compute("status", ["isEnabled", "inputText"], (enabled, text) => {
if (!enabled) return "Input disabled";
if (text.length < 3) return "Input too short (min 3 characters)";
return `Input active: ${text.length} characters`;
});
// Track validation state
// Updates when is enabled and the input text changes
this.compute("isSatusValid", ["isEnabled", "inputText"], (enabled, text) => {
return { [text.length >= 3 || !enabled ? "remove" : "add"]: "text-red-500" };
});
}
}
customElements.define("form-demo", FormDemo);<form-demo>
<div class="space-y-4">
<!-- Toggle input enabled/disabled state -->
<div>
<input type="checkbox" id="enabled" $bind-checked="isEnabled" class="mr-2" />
<label for="enabled">Enable input</label>
</div>
<!-- Input field with validation -->
<div>
<input
type="text"
$bind-value="inputText"
$bind-disabled="isDisabled"
class="border p-2 w-full disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Type when enabled..."
aria-describedby="status"
/>
</div>
<!-- Status message with validation styling -->
<p id="status" role="status">Status: <span $bind-text="status" $bind-class="className"> </span></p>
</div>
</form-demo>Key Features Demonstrated:
- Two-way data binding for form inputs
- Real-time validation with computed properties
- Dynamic enable/disable functionality
- Status messages with validation styling
- Clean separation of form logic and presentation
- Reactive updates without explicit event handling
Custom Binding Handlers
Custom binding handlers allow you to extend the component's binding capabilities with your own custom logic. This powerful feature enables specialized DOM updates based on state changes.
Handler Definition
- Register custom handlers by overriding
customBindingHandlers - Return mapping of binding types to handler functions
- Access element and value details for fine-grained control
- Register custom handlers by overriding
Handler Execution
- Automatically called when bound state changes
- Receives formatted and raw state values
- Full access to elements for direct DOM manipulation
Integration with State System
- Works seamlessly with reactive state management
- Handlers are reactive and update automatically
- Clean integration with the existing binding system
Here's a practical example of custom binding handlers:
class CustomBindingDemo extends ReactiveComponent {
constructor() {
super();
// Initialize state for various custom bindings
this.setState("counter", 0);
this.setState("theme", "light");
this.setState("status", "idle");
}
protected customBindingHandlers(
stateKey: string,
element: HTMLElement,
formattedValue: string,
rawValue: StateValue,
): Record<string, () => void> {
return {
// Custom handler for counter animation
"animate-count": () => {
element.style.transform = `scale(${1 + Number(rawValue) * 0.1})`;
element.textContent = formattedValue;
},
// Custom handler for theme switching
"theme-switch": () => {
const theme = String(rawValue);
element.classList.remove("theme-light", "theme-dark");
element.classList.add(`theme-${theme}`);
element.setAttribute("aria-theme", theme);
},
// Custom handler for status indicators
"status-indicator": () => {
const status = String(rawValue);
element.setAttribute("data-status", status);
element.setAttribute("aria-busy", status === "loading" ? "true" : "false");
element.classList.toggle("pulse", status === "active");
},
};
}
// Methods to update state
increment() {
this.counter++;
}
toggleTheme() {
this.theme = this.theme === "light" ? "dark" : "light";
}
updateStatus(status: string) {
this.status = status;
}
}
customElements.define("custom-binding-demo", CustomBindingDemo);<custom-binding-demo>
<div class="space-y-4">
<!-- Animated counter binding -->
<div>
<span $bind="animate-count:counter" class="text-2xl font-bold transition-transform"> 0 </span>
<button onclick="increment" class="ml-2 px-4 py-2 bg-blue-500 text-white rounded">Increment</button>
</div>
<!-- Theme switching binding -->
<div $bind="theme-switch:theme" class="p-4 border rounded transition-colors">
<h3>Theme Demo</h3>
<button onclick="toggleTheme" class="px-4 py-2 bg-gray-200 rounded">Toggle Theme</button>
</div>
<!-- Status indicator binding -->
<div $bind="status-indicator:status" class="p-2 border rounded">
<p>Current Status: <span $bind-text="status"></span></p>
<div class="flex space-x-2 mt-2">
<button onclick="updateStatus('idle')" class="px-3 py-1 bg-gray-500 text-white rounded">Idle</button>
<button onclick="updateStatus('active')" class="px-3 py-1 bg-green-500 text-white rounded">Active</button>
<button onclick="updateStatus('loading')" class="px-3 py-1 bg-yellow-500 text-white rounded">Loading</button>
</div>
</div>
</div>
</custom-binding-demo>Key Features Demonstrated:
- Custom animation binding with state-based scaling
- Theme switching with dynamic class management
- Multiple custom bindings in a single component
- Clean integration with an existing state system
- Reactive updates without manual event handling
- Accessibility considerations in custom bindings
API Reference
Component Lifecycle
connectedCallback(): Called when the component is added to the DOMdisconnectedCallback(): Called when the component is removed from the DOMattributeChangedCallback(name, oldValue, newValue): Called when an observed attribute changes
State Methods
setState(key: string, value: unknown): Initialize or update state with automatic type coerciongetState(key: string): Retrieve current state value with type safetycompute(key: string, dependencies: string[], computation: Function): Create computed property with dependency trackingeffect(callback: Function): Create a side effect that runs when dependencies changecustomBindingHandlers(stateKey: string, element: HTMLElement, formattedValue: string, rawValue: StateValue): Override to add custom binding handlers for state updates
Context Methods
createContext(stateKey: string): Create a new context object for sharing state between componentsexposeContext(context: Context): Expose state to child components through a context providerconsumeContext(context: Context): Subscribe to a context from a parent component
Element Processing
Processes an element's attributes for special bindings and state declarations. This method is responsible for:
Reference Processing
- Handles
$refattributes to create element references - Populates the component's
refsobject - Automatically removes ref attributes after processing
- Handles
State Declaration Processing
- Process
$stateattributes for direct state declarations - Extracts initial values from element content
- Establishes state-to-element bindings
- Removes state declaration attributes
- Process
Binding Setup
- Handles
$bind-*attributes for two-way data binding - Establishes appropriate event listeners for form elements
- Sets up validation and type coercion
- Removes binding attributes after setup
- Handles
Event Handler Registration
- Process
on*event handler attributes - Binds event handlers to component methods
- Provides event context to handler functions
- Removes event attributes after binding
- Process
Binding Types
| Binding Type | Description | Example |
|---|---|---|
$bind-text | Text content with automatic updates | <span $bind-text="name"></span> |
$bind-html | HTML content with sanitization | <div $bind-html="content"></div> |
$bind-value | Form input value with two-way binding | <input $bind-value="username"> |
$bind-checked | Checkbox/radio state | <input type="checkbox" $bind-checked="isActive"> |
$bind-disabled | Element disabled state | <button $bind-disabled="isLoading">Submit</button> |
$bind-class | Dynamic class binding | <div $bind-class="isActive"> |
$bind-* | Custom state binding type | <div $bind-custom="myState"> |
AI-Assisted Development with System Prompts
ReactiveComponent includes a structured system prompt (system_prompt.xml) that helps developers create robust, accessible web components using AI assistance. This feature provides:
Key Benefits
Structured Development Process
- Standardized component creation workflow
- Built-in validation steps
- Best practices enforcement
Quality Assurance
- Accessibility compliance checks
- Performance optimization guidelines
- Code quality standards
Automated Guidance
- Component architecture recommendations
- State management patterns
- Error handling strategies
Developer Server
Reactive Component uses Query as a developing system that provides bundling, server-side rendering, hot reloading, and state persistence. Here's how they are used together: ReactiveComponent uses Query as a developing system that provides bundling, server-side rendering, hot reloading, and state persistence. Here's how they are used together:
Getting Started
- Start by cloning the ReactiveComponent repository:
git clone https://github.com/gc-victor/reactive-componentThen
cd reactive-componentInstallation
- First, ensure you have all the packages installed:
# Using pnpm (recommended)
pnpm installOr
# Using npm
npm installSetup
Set the local Query settings.
pnpm query settingsInstall assets.
pnpm devThen in a different terminal run:
pnpm query asset publicProject Structure
The project includes:
dist/index.js # Distributed version of the Reactive Component
src
├── index.ts # Reactive Component
├── index.d.ts # Type definitions for Reactive Component
├── public # Public Assets
├── pages # Application pages
│ ├── get.index.tsx # Page server function
│ ├── hot-reload # Hot reload service
│ ├── index.island.js # Examples of Reactive Components
│ ├── layout # Layout components
│ ├── lib # Helper functions
│ └── styles.css # Global styles
└── tests # Test filesIntegration Example
Here's how to integrate the BasicCounter component with Query:
// src/pages/get.index.tsx
import { Page } from "@/pages/layout/page";
export async function handleRequest(req: Request): Promise<Response> {
return response(
<Page>
<div className="container mx-auto p-4">
<basic-counter className="p-4 border rounded block">
<p className="mb-2">
Count: <span $state="count">0</span>
</p>
<button type="button" onClick="decrement" className="mr-2 bg-blue-500 text-white px-4 py-2 rounded">
Decrement
</button>
<button type="button" onClick="increment" className="bg-blue-500 text-white px-4 py-2 rounded">
Increment
</button>
</basic-counter>
</div>
</Page>,
);
}Key Integration Points
- Initial Render
- Server generates complete HTML document
- Web Components are defined in included scripts
- No hydration needed
- State Management
- Initial state can be embedded in HTML attributes
- Web Components handle their own state after initialization
- No explicit hydration or state reconciliation is required
- APIs and Data Flow
- Query functions handle API requests
- Web Components can fetch data through standard APIs
- Database access is controlled through Query's server functions
- Runtime Behavior
- Server provides initial HTML and required scripts
- Web Components take over client-side functionality
- Clean separation between server and client concerns
References
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you want to change.
License
This project is licensed under the MIT License - see the LICENSE file for details.