rescript-signals is a lightweight, high-performance reactive signals library for ReScript with zero dependencies. Build reactive applications with fine-grained updates, automatic dependency tracking, and minimal re-computation, while leveraging ReScript's powerful type system.
npm install rescript-signalsThen, add it to your ReScript project's rescript.json:
{
"dependencies": ["rescript-signals"]
}open Signals
// Create reactive state
let count = Signal.make(0)
// Create a derived state
let doubled = Computed.make(() => Signal.get(count) * 2)
// Logs every time count changes:
Effect.run(() => {
Console.log2("Count is", Signal.get(count))
None // Optional cleanup function
})
// Update the signal
Signal.set(count, 5) // Effect logs: "Count is 5"rescript-signals focuses on clarity, control, and performance. The goal is to offer precise, fine-grained updates and predictable behavior with a minimal set of abstractions.
Reactive state container. Signals track changes and notify dependents automatically when their value changes.
let count = Signal.make(0)
Signal.get(count) // Read with dependency tracking
Signal.peek(count) // Read without tracking
Signal.set(count, 1) // Set a new value
Signal.update(count, n => n + 1) // Update based on current valueDerived reactive values that update automatically. Computed values are lazily evaluated and cached until their dependencies change.
let firstName = Signal.make("Ada")
let lastName = Signal.make("Lovelace")
let fullName = Computed.make(() =>
Signal.get(firstName) ++ " " ++ Signal.get(lastName)
)Side-effect functions that re-run when dependencies change. Effects execute immediately and track any signals read during execution.
Effect.run(() => {
Console.log(Signal.get(count))
Some(() => Console.log("cleanup")) // Optional cleanup
})If you need to pragmatically dispose the effect, you can use Effect.runWithDispose:
let disposer = Effect.runWithDispose(() => {
Console.log(Signal.get(count))
None
})
// Later
disposer.dispose()Group multiple updates with Signal.batch to prevent redundant computations, or skip tracking inside an effect with Signal.untrack:
Signal.batch(() => {
Signal.set(firstName, "Alan")
Signal.set(lastName, "Turing")
}) // Effects run only once
let value = Signal.untrack(() => Signal.get(threshold)) // Read without trackingAll reactive primitives feature automatic dependency tracking — no manual subscriptions needed.
Check the documentation website for the full API reference and examples.
Latest milomg/js-reactivity-benchmark report (other popular frameworks + ReScript Signals), ordered by average runtime per test.
| Rank | Framework | Total ms |
|---|---|---|
| 1 | Alien Signals | 4054.69 |
| 2 | Preact Signals | 4193.43 |
| 3 | ReScript Signals | 5497.12 |
| 4 | Vue | 5909.55 |
| 5 | Svelte v5 | 8163.40 |
| 6 | SolidJS | 10638.23 |
Per-test runtime (ms):
| Framework | 2-10x5 - lazy80% | 25-1000x5 | 3-5x500 | 4-1000x12 - dyn5% | 6-100x15 - dyn50% | 6-10x10 - dyn25% - lazy80% | avoidablePropagation | broadPropagation | cellx1000 | cellx2500 | createComputations | createSignals | deepPropagation | diamond | molBench | mux | repeatedObservers | triangle | unstable | updateSignals |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Alien Signals | 321.62 | 857.38 | 215.96 | 649.03 | 323.65 | 241.78 | 89.86 | 209.09 | 9.44 | 27.88 | 77.39 | 2.69 | 64.15 | 119.16 | 33.33 | 177.25 | 33.39 | 45.73 | 47.95 | 507.98 |
| Preact Signals | 392.88 | 871.31 | 213.01 | 680.89 | 344.58 | 264.95 | 92.54 | 210.91 | 9.71 | 31.13 | 73.09 | 2.07 | 74.11 | 122.93 | 33.17 | 182.45 | 20.35 | 45.69 | 34.15 | 493.50 |
| ReScript Signals | 490.79 | 955.07 | 267.14 | 703.15 | 369.60 | 323.04 | 296.06 | 292.17 | 15.97 | 58.68 | 291.99 | 3.40 | 102.46 | 176.05 | 46.52 | 276.85 | 39.70 | 52.86 | 61.26 | 674.37 |
| Vue | 550.80 | 1156.08 | 300.99 | 904.94 | 451.48 | 361.59 | 197.81 | 267.42 | 24.56 | 74.18 | 117.71 | 2.96 | 123.64 | 181.95 | 72.64 | 268.22 | 27.94 | 64.18 | 51.44 | 709.02 |
| Svelte v5 | 1107.51 | 1192.50 | 300.55 | 1026.17 | 427.26 | 350.29 | 534.55 | 404.05 | 14.86 | 51.73 | 165.07 | 1.81 | 166.14 | 424.79 | 35.19 | 248.15 | 75.66 | 125.05 | 118.99 | 1393.06 |
| SolidJS | 2210.74 | 1650.95 | 510.65 | 1316.11 | 727.56 | 683.84 | 261.95 | 573.50 | 21.75 | 99.62 | 180.46 | 4.36 | 202.17 | 347.05 | 41.07 | 318.85 | 93.33 | 124.10 | 140.36 | 1129.82 |
Note: these are single-machine CI runs and will vary with runner/Node version.
rescript-signals-react provides React hooks for using signals in your components via useSyncExternalStore:
npm install rescript-signals-reactopen SignalsReact
let value = useSignalValue(mySignal) // Subscribe to a signal
let (value, signal, setter) = useSignal(() => 0) // Component-local signal
let derived = useComputed(() => Signal.get(a) + Signal.get(b))See the rescript-signals-react README for full documentation.
| Package | Description |
|---|---|
| rescript-signals | Core reactive signals library |
| rescript-signals-react | React adapter hooks |
See CONTRIBUTING.md for development setup and guidelines.
See LICENSE for details.