Skip to content

Computeds

Derived signals that stay in sync with the values they read.

Computeds are derived signals. They let you describe state in terms of other state instead of manually keeping multiple signals in sync.

Important: Computed.make returns a Signal.t<'a>. You read it with Signal.get or Signal.peek, just like any other signal.

Working with Computeds

Creating Computed Values

A computed is a function plus automatic dependency tracking. Every signal read inside the function becomes an input.

1open Xote
2
3let price = Signal.make(100)
4let quantity = Signal.make(3)
5
6let subtotal = Computed.make(() =>
7 Signal.get(price) * Signal.get(quantity)
8)

If you need to suppress downstream updates for equivalent derived values, Computed.make also accepts ~equals.

Reading Computed Values

Read a computed exactly like a signal. Use Signal.get when the current code should subscribe, or Signal.peek for a one-off read.

1let subtotal = Computed.make(() =>
2 Signal.get(price) * Signal.get(quantity)
3)
4
5let total = Computed.make(() =>
6 Signal.get(subtotal) + 50
7)
8
9Console.log(Signal.get(total))

Lazy Recomputation

When an upstream signal changes, a computed is marked dirty immediately but does not recompute until someone reads it. This keeps unused derived values cheap.

1let count = Signal.make(0)
2
3let doubled = Computed.make(() => {
4 Console.log("recomputing")
5 Signal.get(count) * 2
6})
7
8Signal.set(count, 1)
9// Nothing logged yet
10
11ignore(Signal.get(doubled))
12// Logs "recomputing"

Dynamic Dependencies

Computeds re-track their dependencies every time they run. That means conditionals are allowed: the current control flow determines the active inputs.

1let useMetric = Signal.make(true)
2let celsius = Signal.make(20)
3let fahrenheit = Signal.make(68)
4
5let temperature = Computed.make(() =>
6 if Signal.get(useMetric) {
7 Signal.get(celsius)
8 } else {
9 Signal.get(fahrenheit)
10 }
11)

In Practice

Example: Order Summary

This is a common computed pattern: keep the writable state small, then derive display values like subtotal, shipping, and total from it.

OrderSummary.res
1open Xote
2
3let unitPrice = Signal.make(24)
4let quantity = Signal.make(2)
5let expressShipping = Signal.make(false)
6
7let subtotal = Computed.make(() =>
8 Signal.get(unitPrice) * Signal.get(quantity)
9)
10
11let shippingCost = Computed.make(() =>
12 if Signal.get(expressShipping) {
13 15
14 } else {
15 0
16 }
17)
18
19let total = Computed.make(() =>
20 Signal.get(subtotal) + Signal.get(shippingCost)
21)
22
23let app = () => {
24 <div>
25 <p>
26 {Node.signalText(() => "Subtotal: quot; ++ Int.toString(Signal.get(subtotal)))}
27 </p>
28 <p>
29 {Node.signalText(() => "Shipping: quot; ++ Int.toString(Signal.get(shippingCost)))}
30 </p>
31 <p>
32 {Node.signalText(() => "Total: quot; ++ Int.toString(Signal.get(total)))}
33 </p>
34 </div>
35}

Order Summary

The writable state is unit price, quantity, and shipping mode. Everything else is derived.

Quantity
2
Plan
Shipping
Subtotal$48
Shipping$0
Total$48
fig. 1 - an order summary driven by computeds

Lifecycle

Disposal

Most computeds do not need manual cleanup. They dispose automatically when nothing is subscribed to them anymore. In UI code that usually means they disappear with the DOM that owns them.

Manual Disposal

If you create a long-lived computed outside normal component ownership and want to tear it down explicitly, call Computed.dispose.

1let doubled = Computed.make(() => Signal.get(count) * 2)
2
3// Use it for a while...
4
5Computed.dispose(doubled)

Computed vs Manual Updates

If a value can be derived from other reactive values, prefer a computed instead of mirroring it into another signal.

1// Avoid this
2let count = Signal.make(0)
3let doubled = Signal.make(0)
4
5let increment = () => {
6 Signal.update(count, n => n + 1)
7 Signal.set(doubled, Signal.get(count) * 2)
8}
9
10// Prefer this
11let count = Signal.make(0)
12let doubled = Computed.make(() => Signal.get(count) * 2)

Working Style

Best Practices

  • Keep computeds pure. If the code talks to the outside world, it probably belongs in an effect instead.
  • Read computeds with Signal.get or Signal.peek, because they are signals at the type level.
  • Avoid copying derived values into writable signals unless you truly need editable local state.
  • Reach for custom equality only when downstream updates are too noisy with the default behavior.

Next Steps

  • Read Effects to see where reactive side effects fit on top of signals and computeds.
  • Move to Components when you want to wire derived values into the UI layer.