Signals, Effects & Watchers
Signals are fine-grained reactive values, when a signal changes, only the computations and template bindings that read it re-run. They are ideal for local component state without global stores.
Creating a Signal
Use the t (types) package constructors to create typed signals:
import t "github.com/rfwlab/rfw/v2/types"
count := t.NewInt(0)
name := t.NewString("")
done := t.NewBool(false)
price := t.NewFloat(9.99)
data := t.NewAny(nil)
Read with .Get(), write with .Set():
count.Set(42)
fmt.Println(count.Get()) // 42
The underlying type is *t.Int (alias for *state.Signal[int]), *t.String (alias for *state.Signal[string]), etc. Each signal tracks its own dependents and notifies only them on change.
Wiring Signals with composition.New
When building a component, declare signal fields using their types and composition.New auto-wires them by type detection:
import (
"github.com/rfwlab/rfw/v2/composition"
t "github.com/rfwlab/rfw/v2/types"
)
type Counter struct {
composition.Component
Count *t.Int
Name *t.String
}
func main() {
view, err := composition.New(&Counter{
Count: t.NewInt(0),
Name: t.NewString("hello"),
})
if err != nil {
log.Fatal(err)
}
_ = view
}
If a signal field is nil at construction time, composition.New creates a zero-value signal automatically. You can also initialize signals in OnMount:
func (c *Counter) OnMount() {
c.Count.Set(1)
}
Using Signals in Templates
Read-only binding
Use @signal:Name to display a signal’s current value. The DOM updates whenever the signal changes:
<p>Count: @signal:Count</p>
<p>Name: @signal:Name</p>
Writable binding
Append :w to make form controls write back to the signal:
<input value="@signal:Name:w">
<input type="checkbox" checked="@signal:Done:w">
<textarea>@signal:Bio:w</textarea>
Typing or toggling updates the signal, and every other binding to that signal updates automatically.
Computed expressions with @expr:
Use @expr: for inline computed values. The expression is re-evaluated whenever any referenced signal changes:
<p>Double: @expr:Count.Get * 2</p>
<p>Label: @expr:Name.Get + " world"</p>
The @expr: directive supports arithmetic (+, -, *, /), comparisons (==, !=, <, >, <=, >=), logical operators (&&, ||, !), and field access (.Get).
Conditionals and Loops
Signals integrate with @if: and @for: directives:
@if:signal:Count == "3"
<p>Three!</p>
@endif
@for:item in signal:Items
<li>{item.Text}</li>
@endfor
When Items holds a slice or map, changes patch only the affected DOM nodes.
Effects (Internal)
The state.Effect function is used internally by the framework to track signal dependencies in templates and computed expressions. It is not exposed as a public API for application code. Use lifecycle hooks (OnMount / OnUnmount) or watchers instead.
Passing Signals as Props
Provide signals through component fields so children can bind to them:
view, err := composition.New(&Child{
Count: parentCount,
})
if err != nil {
log.Fatal(err)
}
The child template can then use @signal:Count to stay reactive.
API Reference
| Constructor | Type | Zero value |
|---|---|---|
t.NewInt(v) |
*t.Int |
0 |
t.NewString(v) |
*t.String |
"" |
t.NewBool(v) |
*t.Bool |
false |
t.NewFloat(v) |
*t.Float |
0.0 |
t.NewAny(v) |
*t.Any |
nil |
All signal types support .Get() and .Set().
Why Use Signals
- Local state, no global store needed for component data.
- Fine-grained, only dependents re-run on change.
- Declarative templates, signals bind naturally in RTML.
- Two-way binding,
:wmakes form inputs reactive.
- Computed values,
@expr:derives values from signals inline.