Skip to content

Signals API

Complete API reference for Signal, Computed, and Effect.

Signal API Reference

Complete API documentation for Xote signals.

Type

type t<'a>

A signal is an opaque type representing a reactive state container. The type parameter 'a is the type of value the signal holds.

Functions

make

let make: ('a, ~equals: ('a, 'a) => bool=?) => t<'a>

Creates a new signal with an initial value. Optionally accepts a custom equality function to control when dependents are notified.

Parameters:

  • initialValue: 'a - The initial value for the signal
  • ~equals: ('a, 'a) => bool (optional) - Custom equality function. Returns true if two values should be considered equal (skipping notification). Defaults to strict referential equality (===).

Returns:

  • t<'a> - A new signal

Example:

let count = Signal.make(0)
let name = Signal.make("Alice")
let items = Signal.make([1, 2, 3])

With custom equality:

type position = { x: int, y: int }

let pos = Signal.make(
  { x: 0, y: 0 },
  ~equals=(a, b) => a.x == b.x && a.y == b.y,
)

// Won't notify dependents - values are equal
Signal.set(pos, { x: 0, y: 0 })

// Will notify dependents - y changed
Signal.set(pos, { x: 0, y: 1 })

get

let get: t<'a> => 'a

Reads the current value from a signal. When called inside a tracking context (effect or computed), automatically registers the signal as a dependency.

Parameters:

  • signal: t<'a> - The signal to read from

Returns:

  • 'a - The current value

Example:

let count = Signal.make(5)
let value = Signal.get(count) // Returns 5

Effect.run(() => {
  // Creates a dependency on count
  Console.log(Signal.get(count))
  None
})

Note: Always creates a dependency when called in a tracking context. Use peek() to read without tracking.


peek

let peek: t<'a> => 'a

Reads the current value from a signal without creating a dependency, even in tracking contexts.

Parameters:

  • signal: t<'a> - The signal to read from

Returns:

  • 'a - The current value

Example:

let count = Signal.make(5)

Effect.run(() => {
  // Does NOT create a dependency
  let value = Signal.peek(count)
  Console.log(value)
  None
})

Signal.set(count, 10) // Effect will NOT re-run

Use cases:

  • Reading signals in effects without creating dependencies
  • Debugging (logging signal values without tracking)
  • Reading configuration values that don't need to trigger updates

set

let set: (t<'a>, 'a) => unit

Sets a new value for the signal and notifies all dependent observers if the value has changed.

Parameters:

  • signal: t<'a> - The signal to update
  • value: 'a - The new value

Returns:

  • unit

Example:

let count = Signal.make(0)
Signal.set(count, 10) // count is now 10, observers notified

Signal.set(count, 10) // Same value - no notification

Equality Check: Uses the signal's equality function to check if the value has changed. By default, this is strict referential equality (===). Only notifies dependent observers if the new value differs from the current value. This prevents unnecessary recomputations and helps avoid infinite loops when effects write back to their dependencies.

Note: A custom equality function can be provided when creating the signal via Signal.make(value, ~equals=...). See make above for details and examples.


update

let update: (t<'a>, 'a => 'a) => unit

Updates a signal's value based on its current value.

Parameters:

  • signal: t<'a> - The signal to update
  • fn: 'a => 'a - Function that receives the current value and returns the new value

Returns:

  • unit

Example:

let count = Signal.make(0)
Signal.update(count, n => n + 1) // count is now 1
Signal.update(count, n => n * 2) // count is now 2

let items = Signal.make([1, 2, 3])
Signal.update(items, arr => Array.concat(arr, [4, 5])) // [1, 2, 3, 4, 5]

Note: Equivalent to Signal.set(signal, fn(Signal.get(signal))) but more concise.


batch

let batch: (unit => 'a) => 'a

Groups multiple signal updates together, ensuring observers run only once after all updates complete.

Parameters:

  • fn: unit => 'a - Function containing signal updates

Returns:

  • 'a - The return value of the function

Example:

Signal.batch(() => {
  Signal.set(firstName, "Jane")
  Signal.set(lastName, "Smith")
})
// Observers run once with both updates

untrack

let untrack: (unit => 'a) => 'a

Executes a function without tracking any signal dependencies.

Parameters:

  • fn: unit => 'a - Function to execute untracked

Returns:

  • 'a - The return value of the function

Example:

Effect.run(() => {
  let tracked = Signal.get(count)

  Signal.untrack(() => {
    let untracked = Signal.get(otherSignal) // Not tracked
  })

  None
})

Examples

Basic Usage

open Xote

let count = Signal.make(0)

// Read
Console.log(Signal.get(count)) // 0

// Update
Signal.set(count, 5)
Console.log(Signal.get(count)) // 5

// Update based on current value
Signal.update(count, n => n + 1)
Console.log(Signal.get(count)) // 6

With Effects

let count = Signal.make(0)

Effect.run(() => {
  Console.log2("Count changed:", Signal.get(count))
  None
})

Signal.set(count, 1) // Logs: "Count changed: 1"
Signal.set(count, 2) // Logs: "Count changed: 2"

With Computed

let count = Signal.make(5)
let doubled = Computed.make(() => Signal.get(count) * 2)

Console.log(Signal.get(doubled)) // 10

Signal.set(count, 10)
Console.log(Signal.get(doubled)) // 20

Complex State

type user = {
  id: int,
  name: string,
  email: string,
}

let user = Signal.make({
  id: 1,
  name: "Alice",
  email: "alice@example.com",
})

// Update specific fields
Signal.update(user, u => {...u, name: "Alice Smith"})
Signal.update(user, u => {...u, email: "alice.smith@example.com"})

Array Operations

let todos = Signal.make([])

// Add item
Signal.update(todos, arr => Array.concat(arr, ["Buy milk"]))

// Remove item
Signal.update(todos, arr => Array.filter(arr, item => item != "Buy milk"))

// Update item
Signal.update(todos, arr =>
  Array.map(arr, item =>
    item == "Buy milk" ? "Buy oat milk" : item
  )
)

Notes

  • Signals use strict referential equality (===) by default - only notify dependents when the value actually changes. Use ~equals for custom comparison logic.
  • Use peek() to avoid creating dependencies in effects
  • Signals work with any type: primitives, records, arrays, etc.
  • Use Signal.batch() to group multiple updates
  • The equality check prevents accidental infinite loops and unnecessary recomputations

See Also

Was this page helpful?