@signal
@signal declares a host-aware writable signal field.
The decorated member becomes a real WritableSignal instance that JSX can consume directly in child or attribute positions.
Use it when the field itself should behave like a signal object. If you only need ordinary reactive host state, use @state instead.
Example
/** @jsxImportSource @ecopages/jsx */
import { RadiantElement, customElement } from '@ecopages/radiant';
import { signal } from '@ecopages/radiant/decorators/signal';
@customElement('signal-counter')
export class SignalCounter extends RadiantElement {
@signal count = 0;
private readonly increment = () => {
this.count.update((value) => value + 1);
};
override render() {
return (
<section>
<p>Count: {this.count}</p>
<button type="button" on:click={this.increment}>
Increment
</button>
</section>
);
}
}Options
| Option | Type | Description |
|---|---|---|
bind | boolean | string | Exposes a JSX binding companion such as $count or a custom binding name |
initial | T | Optional initial value when the field does not provide one directly |
source | WritableSignal<T> | (host) => WritableSignal<T> | Connects an existing writable signal instead of creating a host-owned one |
hydrate | String | Number | Boolean | Object | Array | Serializes the current signal value into SSR host output and restores it during hydration |
What It Changes
- the field becomes a real writable signal instance
- connected signals still flow through Radiant's update callback channel
@onUpdated(...)and JSX bindings keep working with the same host update model- on render-owning
RadiantElementhosts, signal and store reads performed duringrender()participate in rerender invalidation directly
That last point is the main difference from @state: a render method can read the signal directly and let the signals runtime invalidate the view.
override render() {
return <p>Count: {this.count}</p>;
}If the signal changes through this.count.set(...), the rendered output updates without an extra imperative sync step.
Shared Signals
You can connect an existing signal instead of creating a host-owned one.
import { State } from '@ecopages/signals';
import { RadiantElement, customElement } from '@ecopages/radiant';
import { signal } from '@ecopages/radiant/decorators/signal';
const sharedCount = new State(0);
@customElement('shared-counter')
export class SharedCounter extends RadiantElement {
@signal({ source: sharedCount }) declare count: State<number>;
}Relationship To @state
- Use
@statewhen the member should stay a plain host field managed by Radiant's reactive field system. - Use
@signalwhen the member itself should be a writable signal value.
Related Subpaths
Signal primitives come from @ecopages/signals.
If you need host-owned async state built on top of signals, use createResource from @ecopages/radiant (documented in Resources).
See Signals Overview for the underlying signal model.