SubscriptionRef
A SubscriptionRef<A> is a Ref you can observe. It
holds the current value like any reference, but it also exposes a
Stream of changes: subscribe to it and you receive the current
value immediately, followed by every subsequent update.
This is the bridge between mutable state and reactive consumers. Use it for state that drives a UI, a live dashboard, a health indicator — anything that needs to react to changes rather than poll for them.
Observing changes
Section titled “Observing changes”The defining operation is changes, which returns a Stream that replays the
current value and then emits each future update.
import { Deferred, Effect, Fiber, Stream, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) // Used to make sure the subscriber is attached before we start updating. const ready = yield* Deferred.make<void>()
// Fork a fiber that collects the first three values it observes. const fiber = yield* SubscriptionRef.changes(ref).pipe( // Signal "ready" as soon as the first (current) value arrives. Stream.tap(() => Deferred.succeed(ready, void 0)), Stream.take(3), Stream.runCollect, Effect.forkChild )
// Wait until the subscriber has received the initial value... yield* Deferred.await(ready) // ...then push two updates; both are published to the subscriber. yield* SubscriptionRef.set(ref, 1) yield* SubscriptionRef.set(ref, 2)
return yield* Fiber.join(fiber) // [0, 1, 2]})The subscriber sees 0 (the value at the moment it subscribed), then 1 and 2.
A subscriber that attaches later would still receive the latest value first,
because SubscriptionRef replays the most recent value to every new subscriber.
Updating the value
Section titled “Updating the value”SubscriptionRef carries the full Ref-style update API, and every successful
update is published to current subscribers. Because updates run under an internal
semaphore, the effectful variants (updateEffect, modifyEffect, …) are
serialized the same way SynchronizedRef
serializes them.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10)
// Pure update — publishes the new value (20) to subscribers. yield* SubscriptionRef.update(ref, (n) => n * 2)
// Effectful update — serialized, and also published once it commits. yield* SubscriptionRef.updateEffect(ref, (n) => Effect.succeed(n + 5))
return yield* SubscriptionRef.get(ref) // 25})Reactive shared state through a service
Section titled “Reactive shared state through a service”The natural home for a SubscriptionRef is a service
that owns a piece of shared state and lets the rest of the app both mutate it and
subscribe to it. Here a service tracks application status; a background worker
flips the status while a logger reacts to every transition.
import { Context, Effect, Fiber, Layer, Stream, SubscriptionRef } from "effect"
type Status = "starting" | "ready" | "degraded"
class Health extends Context.Service<Health, { readonly set: (status: Status) => Effect.Effect<void> readonly changes: Stream.Stream<Status>}>()("app/Health") { static layer = Layer.effect( Health, Effect.gen(function*() { const ref = yield* SubscriptionRef.make<Status>("starting") return Health.of({ set: (status) => SubscriptionRef.set(ref, status), // Expose the change stream so any consumer can react to transitions. changes: SubscriptionRef.changes(ref) }) }) )}
const program = Effect.gen(function*() { const health = yield* Health
// A consumer that logs every status it observes, including the initial one. const logger = yield* health.changes.pipe( Stream.take(3), Stream.runForEach((status) => Effect.log(`status -> ${status}`)), Effect.forkChild )
// Drive some transitions; each is published to the logger above. yield* health.set("ready") yield* health.set("degraded")
yield* Fiber.join(logger)}).pipe(Effect.provide(Health.layer))The logger prints starting, then ready, then degraded — it reacts to state
changes without ever polling, and any number of consumers can subscribe
independently.
When to use it
Section titled “When to use it”Choose SubscriptionRef when something needs to observe state as it evolves. If
you only ever read the current value, a plain Ref is
simpler and lighter. If you need serialized effectful updates but no subscribers,
use SynchronizedRef. For everything you
can build on top of the change stream, see Streaming.