Ref
A Ref<A> is a fiber-safe mutable cell holding a single value of type A. It is
the workhorse of Effect state management: a controlled, atomic alternative to a
plain mutable variable. Reads, writes, and transformations are all effects, so
they sequence with the rest of your program and stay correct when several fibers
touch the same Ref concurrently.
Reach for a Ref whenever the next value is a pure function of the current one
— a counter, an accumulator, a small piece of in-memory configuration.
Creating and updating a Ref
Section titled “Creating and updating a Ref”import { Effect, Ref } from "effect"
const program = Effect.gen(function*() { // `make` returns an effect, because allocating shared state is itself an // effect — it must happen at a well-defined point in the program. const counter = yield* Ref.make(0)
// `update` reads, transforms, and writes back in one atomic step. Prefer it // over a separate `get` + `set` whenever the new value depends on the old. yield* Ref.update(counter, (n) => n + 1)
// `get` reads the current value without changing it. return yield* Ref.get(counter) // 1})The core operations
Section titled “The core operations”A handful of operations cover almost everything:
Ref.get/Ref.set— read the value, or replace it with a known value.Ref.update— replace the value withf(current), atomically. Use this whenever the new value depends on the old one.Ref.modify— atomically compute two things from the current value: a return value and the next stored value.
modify is the most general operation. It takes a function returning a
[result, newValue] tuple, stores newValue, and hands you back result. This
lets you read-and-update in a single atomic step — useful for things like handing
out unique IDs:
import { Effect, Ref } from "effect"
const program = Effect.gen(function*() { const nextId = yield* Ref.make(1)
// Atomically take the current id and advance the counter. No two fibers can // ever observe the same id, even calling this concurrently. const allocate = Ref.modify(nextId, (id) => [id, id + 1] as const)
const a = yield* allocate // 1 const b = yield* allocate // 2 return [a, b]})Each operation also comes in andGet / getAnd variants when you want the new or
previous value back: updateAndGet, getAndUpdate, getAndSet, setAndGet.
The Some variants (updateSome, modifySome, getAndUpdateSome) take a
function returning an Option and leave the value untouched when it returns
Option.none(), which is handy for conditional state transitions.
Sharing a Ref through a service
Section titled “Sharing a Ref through a service”A Ref is rarely passed around by hand. Instead, wrap it in a
service so any subprogram can reach the same shared
state. This is the idiomatic v4 way to expose mutable state, and it keeps the
operations you allow on the state in one place.
import { Context, Effect, Layer, Ref } from "effect"
// A small counter service. The Ref stays private inside the layer; consumers// only see the operations we choose to expose.class Counter extends Context.Service<Counter, { readonly increment: Effect.Effect<void> readonly decrement: Effect.Effect<void> readonly current: Effect.Effect<number>}>()("app/Counter") { // The layer allocates the Ref once, when the service is constructed. static layer = Layer.effect( Counter, Effect.gen(function*() { const ref = yield* Ref.make(0) return Counter.of({ increment: Ref.update(ref, (n) => n + 1), decrement: Ref.update(ref, (n) => n - 1), current: Ref.get(ref) }) }) )}
const program = Effect.gen(function*() { const counter = yield* Counter yield* counter.increment yield* counter.increment yield* counter.decrement return yield* counter.current // 1})
// Every fiber that runs with this layer shares the same underlying Ref.const runnable = program.pipe(Effect.provide(Counter.layer))Because the service exposes only increment, decrement, and current, callers
cannot set the counter to an arbitrary value — the Ref is an implementation
detail.
Using a Ref across fibers
Section titled “Using a Ref across fibers”A Ref shines when several fibers update the same state at once. Each individual
update is atomic, so concurrent increments cannot clobber one another.
import { Effect, Fiber, Ref } from "effect"
const program = Effect.gen(function*() { const ref = yield* Ref.make(0)
// Fork two fibers that each increment the shared counter many times. const work = Effect.forEach( Array.from({ length: 100 }, (_, i) => i), () => Ref.update(ref, (n) => n + 1), { discard: true } )
const fiber1 = yield* Effect.forkChild(work) const fiber2 = yield* Effect.forkChild(work)
// Wait for both to finish before reading the final total. yield* Fiber.join(fiber1) yield* Fiber.join(fiber2)
return yield* Ref.get(ref) // 200 — no updates were lost})The result is always 200: because update is atomic, the two fibers’
increments are correctly interleaved rather than racing on a read-modify-write.
When a Ref is not enough
Section titled “When a Ref is not enough”A plain Ref cannot run an effect to compute its next value while staying
serialized — its update functions must be pure. If your update needs to perform
an effect (call an API, read the Clock) and must not interleave
with other updates, use SynchronizedRef.
If other parts of the program need to observe every change as a stream, use
SubscriptionRef.