Skip to content

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.

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.

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
})

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.

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.