Signals
Reactive state cells that form the foundation of Xote's reactivity model.
Signals
Signals are the foundation of reactive state in Xote. A signal is a reactive state container that automatically notifies its dependents when its value changes.
Info: Xote re-exports Signal, Computed, and Effect from rescript-signals. The API and behavior are provided by that library.
Creating Signals
Use Signal.make() to create a new signal with an initial value:
open Xote
let count = Signal.make(0)
let name = Signal.make("Alice")
let isActive = Signal.make(true)Reading Signal Values
Signal.get()
Use Signal.get() to read a signal's value. When called inside a tracking context (like an effect or computed value), it automatically registers the signal as a dependency:
let count = Signal.make(5)
let value = Signal.get(count) // Returns 5Signal.peek()
Use Signal.peek() to read a signal's value without creating a dependency:
let count = Signal.make(5)
Effect.run(() => {
// This creates a dependency
let current = Signal.get(count)
// This does NOT create a dependency
let peeked = Signal.peek(count)
Console.log2("Current:", current)
Console.log2("Peeked:", peeked)
})Updating Signals
Signal.set()
Replace a signal's value entirely:
let count = Signal.make(0)
Signal.set(count, 10) // count is now 10Signal.update()
Update a signal based on its current value:
let count = Signal.make(0)
Signal.update(count, n => n + 1) // count is now 1
Signal.update(count, n => n * 2) // count is now 2Important Behaviors
Structural Equality Check
Signals use structural equality (==) to check if a value has changed. If the new value equals the old value, dependents are not notified:
let count = Signal.make(5)
Effect.run(() => {
Console.log(Signal.get(count))
})
Signal.set(count, 5) // Effect does NOT run - value didn't change
Signal.set(count, 6) // Effect runs - value changedThis prevents unnecessary updates and helps avoid accidental infinite loops in reactive code.
Custom Equality Check
By default, signals use strict referential equality (===) to determine if a value has changed. For complex types like records or objects where structurally equivalent values may have different references, you can provide a custom equality function via the ~equals parameter:
type position = { x: int, y: int }
// Without custom equality: every set() triggers updates,
// even if x and y are the same
let pos1 = Signal.make({ x: 0, y: 0 })
// With custom equality: only triggers updates when
// x or y actually change
let pos2 = Signal.make(
{ x: 0, y: 0 },
~equals=(a, b) => a.x == b.x && a.y == b.y,
)
Effect.run(() => {
let { x, y } = Signal.get(pos2)
Console.log(`Position: ${Int.toString(x)}, ${Int.toString(y)}`)
None
})
// This will NOT trigger the effect - values are equal
Signal.set(pos2, { x: 0, y: 0 })
// This WILL trigger the effect - y changed
Signal.set(pos2, { x: 0, y: 1 })Custom equality is useful when working with records, tuples, or other compound types where you want to compare by value rather than by reference.
Automatic Dependency Tracking
When you call Signal.get() inside a tracking context, the dependency is automatically registered:
let firstName = Signal.make("John")
let lastName = Signal.make("Doe")
// This computed automatically depends on both firstName and lastName
let fullName = Computed.make(() =>
Signal.get(firstName) ++ " " ++ Signal.get(lastName)
)Example: Counter
Here's a complete example showing signals in action:
open Xote
let count = Signal.make(0)
let increment = (_evt: Dom.event) => {
Signal.update(count, n => n + 1)
}
let decrement = (_evt: Dom.event) => {
Signal.update(count, n => n - 1)
}
let reset = (_evt: Dom.event) => {
Signal.set(count, 0)
}
let app = () => {
<div>
<h1>
{Node.signalText(() => "Count: " ++ Int.toString(Signal.get(count)))}
</h1>
<button onClick={increment}>
{Node.text("+")}
</button>
<button onClick={decrement}>
{Node.text("-")}
</button>
<button onClick={reset}>
{Node.text("Reset")}
</button>
</div>
}
Node.mountById(app(), "app")Best Practices
- Keep signals focused: Each signal should represent a single piece of state
- Use peek() to avoid dependencies: When you need to read a value without tracking, use peek()
- Prefer update() over get() + set(): It's more concise and clearer in intent
Next Steps
- Learn about Computed Values for derived state
- Understand Effects for side effects
- See the API Reference for complete signal API