Reactivity Fundamentals
rfw updates the DOM only where state changed. No virtual DOM, no manual patching.
Signals
Signals are standalone reactive values. Create them with typed constructors:
import t "github.com/rfwlab/rfw/v2/types"
count := t.NewInt(0)
name := t.NewString("rfw")
active := t.NewBool(false)
Get() reads, Set() writes and notifies dependents:
count.Set(count.Get() + 1)
Signals are nil-safe: calling .Get() or .Set() on a nil *Signal[T] is a no-op (Get returns zero value).
In components, declare signal fields by type — no tags required:
type Counter struct {
composition.Component
Count t.Int // value type
Name *t.String // pointer type (auto-initialized if nil)
}
composition.New(&Counter{}) detects signal-type fields and auto-wires them. Nil pointer fields get zero-value signals.
In RTML, bind signals with @signal:name:
<p>@signal:Count</p>
<input value="@signal:Count:w">
Append :w for two-way binding on form controls.
Host Signal Types (SSC)
For server-side computed values, use host signal types:
type VisitPage struct {
composition.Component
Visit t.HInt // signal + host component binding
}
t.HInt, t.HString, t.HBool, t.HFloat are signals that also auto-register as host component bindings for SSC.
Stores
Stores hold shared reactive key/value pairs scoped to a component. Declare them with *t.Store fields:
type App struct {
composition.Component
CounterStore *t.Store
}
composition.New creates or retrieves the store automatically. Access it via @store in RTML:
<p>@store:default.counter.count</p>
<input value="@store:default.counter.count:w">
Stores can also be created manually:
func (a *App) OnMount() {
s := a.CounterStore
s.Set("count", 0)
}
Template Directives
| Directive | Writable | Purpose |
|---|---|---|
@signal:Name |
No | Read a signal prop |
@signal:Name:w |
Yes | Two-way bind to form control |
@store:module.store.key |
No | Read a store value |
@store:module.store.key:w |
Yes | Two-way bind to store |
@expr:expression |
No | Computed expression |
@on:event:handler |
- | DOM event to Go method |
@expr: Computed Expressions
RTML supports inline expressions with @expr::
<p>@expr:Count.Get * 2</p>
<p>@expr:Count.Get > 0</p>
<p>@expr:Name.Get + "!"</p>
These re-evaluate whenever any referenced signal changes. Prefer @expr: for simple derivations; use Go methods for complex logic.
Fine-grained Updates
When a signal or store value changes, rfw patches only the DOM nodes that depend on it. Unrelated nodes remain untouched. This scales to large applications without re-rendering the entire component tree.
Effects
state.Effect runs a function and re-runs it when accessed signals change:
state.Effect(func() func() {
fmt.Println("count:", count.Get())
return nil
})
Returns a stop function. Optional cleanup runs before each re-execution.
Summary
- Use signal types (
t.Int,*t.String) for local reactive state;*t.Storefor component-scoped shared state.
@signal:Name:wand@store:...:wenable two-way binding on inputs.
@expr:handles inline computed values in templates.
- Changes propagate only to affected nodes.
- Host signal types (
t.HInt, etc.) add SSC host bindings.
- All signal methods are nil-safe.