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.

A SubscriptionRef<A> has the same operation surface as SynchronizedRef — the full set of pure operations plus their *Effect variants, all serialized under an internal semaphore so concurrent updates never interleave — plus the observable changes stream. Every successful set, every non-empty update, and every committed modify publishes the new value to current subscribers.

Type guard that returns true when the value is a SubscriptionRef. Useful for narrowing an unknown before calling subscription-ref operations.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(0)
SubscriptionRef.isSubscriptionRef(ref) // => true
SubscriptionRef.isSubscriptionRef(42) // => false
})

Constructs a SubscriptionRef from an initial value. The initial value is published during construction, so changes starts new subscribers with it.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(0)
return yield* SubscriptionRef.get(ref) // => 0
})

Returns a Stream that first emits the current value and then emits every subsequent published value. Each new subscriber independently receives the latest value first thanks to the replay buffer.

import { Effect, Fiber, Stream, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(0)
// Subscribe (replays the current value 0), then drive an update.
const fiber = yield* SubscriptionRef.changes(ref).pipe(
Stream.take(2),
Stream.runCollect,
Effect.forkChild
)
yield* SubscriptionRef.set(ref, 1)
return yield* Fiber.join(fiber) // => [0, 1]
})

Reads the current value once, without subscribing. Equivalent to Ref.get.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(42)
return yield* SubscriptionRef.get(ref) // => 42
})

Reads the current value synchronously, bypassing the semaphore. Only safe when the caller is certain there are no concurrent modifications.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(42)
return SubscriptionRef.getUnsafe(ref) // => 42 (plain value, not an Effect)
})

Replaces the value and publishes it to subscribers.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(0)
yield* SubscriptionRef.set(ref, 42)
return yield* SubscriptionRef.get(ref) // => 42
})

Sets a new value and returns the previous value, publishing the change.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.getAndSet(ref, 20) // => 10 (old value)
})

Sets a new value and returns that new value, publishing the change.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(0)
return yield* SubscriptionRef.setAndGet(ref, 42) // => 42
})

Applies a function to the current value and publishes the result.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
yield* SubscriptionRef.update(ref, (n) => n * 2)
return yield* SubscriptionRef.get(ref) // => 20
})

Applies a function and returns the new value, publishing the change.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.updateAndGet(ref, (n) => n * 2) // => 20
})

Applies a function and returns the previous value, publishing the change.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.getAndUpdate(ref, (n) => n * 2) // => 10 (old value)
})

Applies a partial update. On Option.some the value is set and published; on Option.none the reference is left unchanged and nothing is published.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
yield* SubscriptionRef.updateSome(
ref,
(n) => n > 5 ? Option.some(n * 2) : Option.none()
)
return yield* SubscriptionRef.get(ref) // => 20
})

Like updateSome, but returns the value after the update decision — the new value on some, or the unchanged current value on none.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.updateSomeAndGet(
ref,
(n) => n > 5 ? Option.some(n * 2) : Option.none()
) // => 20
})

Like updateSome, but returns the previous value regardless of whether the update was applied.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.getAndUpdateSome(
ref,
(n) => n > 5 ? Option.some(n * 2) : Option.none()
) // => 10 (old value)
})

Computes a [result, newValue] pair: the new value is set and published, and the separate result is returned. Use it to read and write in one atomic step.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.modify(ref, (n) => [`was ${n}`, n * 2])
// => "was 10" (ref now holds 20)
})

Like modify, but the second element of the pair is an Option. On Option.some the value is updated and published; on Option.none only the result is returned and nothing is published.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.modifySome(
ref,
(n) => n > 5 ? ["updated", Option.some(n * 2)] : ["unchanged", Option.none()]
) // => "updated" (ref now holds 20)
})

Effectful variant of update. The effect runs under the semaphore; the produced value is set and published once it commits.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
yield* SubscriptionRef.updateEffect(ref, (n) => Effect.succeed(n + 5))
return yield* SubscriptionRef.get(ref) // => 15
})

Effectful variant of updateAndGet — returns the new value.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.updateAndGetEffect(
ref,
(n) => Effect.succeed(n + 5)
) // => 15
})

Effectful variant of getAndUpdate — returns the previous value.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.getAndUpdateEffect(
ref,
(n) => Effect.succeed(n + 5)
) // => 10 (old value)
})

Effectful variant of updateSome. On effectful Option.some the value is set and published; on Option.none nothing changes.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
yield* SubscriptionRef.updateSomeEffect(
ref,
(n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none())
)
return yield* SubscriptionRef.get(ref) // => 13
})

Effectful variant of updateSomeAndGet — returns the value after the update decision.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.updateSomeAndGetEffect(
ref,
(n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none())
) // => 13
})

Effectful variant of getAndUpdateSome — returns the previous value.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.getAndUpdateSomeEffect(
ref,
(n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none())
) // => 10 (old value)
})

Effectful variant of modify. The effect yields a [result, newValue] pair; the new value is set and published, the result returned.

import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.modifyEffect(
ref,
(n) => Effect.succeed([`doubled ${n}`, n * 2] as const)
) // => "doubled 10" (ref now holds 20)
})

Effectful variant of modifySome. The effect yields a [result, Option<newValue>] pair; the value is published only on Option.some.

import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() {
const ref = yield* SubscriptionRef.make(10)
return yield* SubscriptionRef.modifySomeEffect(
ref,
(n) =>
Effect.succeed(
n > 5
? (["updated", Option.some(n + 5)] as const)
: (["unchanged", Option.none()] as const)
)
) // => "updated" (ref now holds 15)
})