Signals
State containers that drive Xote's reactive graph.
Signals are the state primitive in Xote. A signal stores a value, tracks who reads it, and notifies dependents when the value actually changes.
Info: Xote re-exports Signal, Computed, and Effect from rescript-signals.
Working with Signals
Creating Signals
Use Signal.make to create a signal. You can optionally pass ~name for debugging and ~equals when the default equality is not enough.
1open Xote23let count = Signal.make(0)4let userName = Signal.make("Ada", ~name="user-name")5let settings = Signal.make({6 theme: "dark",7 compact: false,8})Reading Signal Values
There are two read modes. Choose based on whether the current code should subscribe to future updates.
Signal.get()
Use Signal.get inside a computed, effect, or reactive node when the current code should re-run if the signal changes.
1let firstName = Signal.make("Ada")2let lastName = Signal.make("Lovelace")34let fullName = Computed.make(() =>5 Signal.get(firstName) ++ " " ++ Signal.get(lastName)6)Signal.peek()
Use Signal.peek when you need the current value without subscribing. This is useful for logging, snapshots, and one-off reads inside effects.
1Effect.run(() => {2 let tracked = Signal.get(count)3 let snapshot = Signal.peek(settings)45 Console.log2("Tracked count:", tracked)6 Console.log2("Current theme:", snapshot.theme)7 None8})Updating Signals
Signal.set()
Use Signal.set when you already know the next value.
1Signal.set(count, 10)2Signal.set(userName, "Grace")Signal.update()
Use Signal.update when the next value depends on the current one. This keeps the intent obvious and avoids an extra read.
1Signal.update(count, n => n + 1)23Signal.update(settings, current => {4 ...current,5 compact: !current.compact,6})How Signals Decide to Update
Equality and Change Detection
Signals only notify dependents when the new value is considered different from the current value. By default that check uses JavaScript strict equality, ===.
Default Equality
For primitives, setting the same value is a no-op. For arrays, records, and objects, a new reference counts as a change even when the fields look the same.
1let count = Signal.make(5)23Signal.set(count, 5) // No update4Signal.set(count, 6) // Notifies dependents56let items = Signal.make([1, 2, 3])7Signal.set(items, [1, 2, 3]) // New array reference, so this updatesCustom Equality
When you want value-based comparison for compound data, pass ~equals to Signal.make.
1type position = {x: int, y: int}23let position = Signal.make(4 {x: 0, y: 0},5 ~equals=(a, b) => a.x == b.x && a.y == b.y,6)78Signal.set(position, {x: 0, y: 0}) // No update9Signal.set(position, {x: 0, y: 1}) // UpdateDependency Tracking
Every Signal.get call inside an active computed or effect becomes a dependency. On the next run, dependencies are cleared and tracked again, so the graph follows control flow instead of staying fixed forever.
1let useMetric = Signal.make(true)2let celsius = Signal.make(20)3let fahrenheit = Signal.make(68)45let temperature = Computed.make(() =>6 if Signal.get(useMetric) {7 Signal.get(celsius)8 } else {9 Signal.get(fahrenheit)10 }11)In Practice
Example: Counter
This is the same pattern most Xote state starts with: a signal, a few updates, and a reactive read in the UI.
1open Xote23let count = Signal.make(0)45let increment = (_evt: Dom.event) => {6 Signal.update(count, n => n + 1)7}89let decrement = (_evt: Dom.event) => {10 Signal.update(count, n => n - 1)11}1213let reset = (_evt: Dom.event) => {14 Signal.set(count, 0)15}1617let app = () => {18 <div>19 <h1>20 {Node.signalText(() => "Count: " ++ Int.toString(Signal.get(count)))}21 </h1>22 <button onClick={increment}>23 {Node.text("+")}24 </button>25 <button onClick={decrement}>26 {Node.text("-")}27 </button>28 <button onClick={reset}>29 {Node.text("Reset")}30 </button>31 </div>32}3334Node.mountById(app(), "app")Counter
Working Style
Best Practices
- Keep one signal focused on one job. A small record is fine; a grab-bag of unrelated state is not.
- Prefer
Signal.updatewhen the next value depends on the current one. - Treat
Signal.peekas a snapshot tool, not your default read API. - Add custom equality only when strict equality creates real noise in the UI or effects.
Next Steps
- Read Computeds if the next question is how to derive state instead of storing it twice.
- Read Effects if you need to connect signals to timers, network requests, or browser APIs.
- Use the Signals API reference when you want signatures and quick examples.