Ref is a mutable reference that provides safe concurrent access and modification of a shared value in Effect programs.
Unlike FiberRef (which is fiber-local), Ref is shared across all fibers and provides atomic operations for managing concurrent state.
Type
interface Ref<in out A> extends Effect.Effect<A>
Ref<A> holds a mutable value of type A.
Creating Refs
make
Creates a newRef with an initial value.
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(0)
yield* Ref.set(ref, 42)
const value = yield* Ref.get(ref)
console.log(value) // 42
})
unsafeMake
Unsafely creates a newRef.
const unsafeMake: <A>(value: A) => Ref<A>
Reading Values
get
Gets the current value.const get: <A>(self: Ref<A>) => Effect.Effect<A>
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(100)
const value = yield* Ref.get(ref)
console.log(value) // 100
})
Writing Values
set
Sets a new value.const set: {
<A>(value: A): (self: Ref<A>) => Effect.Effect<void>
<A>(self: Ref<A>, value: A): Effect.Effect<void>
}
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(0)
yield* Ref.set(ref, 100)
const value = yield* Ref.get(ref)
console.log(value) // 100
})
update
Atomically updates the value using a function.const update: {
<A>(f: (a: A) => A): (self: Ref<A>) => Effect.Effect<void>
<A>(self: Ref<A>, f: (a: A) => A): Effect.Effect<void>
}
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const counter = yield* Ref.make(0)
yield* Ref.update(counter, n => n + 1)
yield* Ref.update(counter, n => n + 1)
const value = yield* Ref.get(counter)
console.log(value) // 2
})
modify
Atomically modifies the value and returns a result.const modify: {
<A, B>(f: (a: A) => readonly [B, A]): (self: Ref<A>) => Effect.Effect<B>
<A, B>(self: Ref<A>, f: (a: A) => readonly [B, A]): Effect.Effect<B>
}
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(10)
const oldValue = yield* Ref.modify(ref, n => [n, n * 2])
console.log(oldValue) // 10
const newValue = yield* Ref.get(ref)
console.log(newValue) // 20
})
Atomic Get-and-Set Operations
getAndSet
Atomically gets the current value and sets a new one.const getAndSet: {
<A>(value: A): (self: Ref<A>) => Effect.Effect<A>
<A>(self: Ref<A>, value: A): Effect.Effect<A>
}
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(5)
const old = yield* Ref.getAndSet(ref, 10)
console.log(old) // 5
const current = yield* Ref.get(ref)
console.log(current) // 10
})
getAndUpdate
Atomically gets the current value and updates it.const getAndUpdate: {
<A>(f: (a: A) => A): (self: Ref<A>) => Effect.Effect<A>
<A>(self: Ref<A>, f: (a: A) => A): Effect.Effect<A>
}
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(3)
const old = yield* Ref.getAndUpdate(ref, n => n * 2)
console.log(old) // 3
const current = yield* Ref.get(ref)
console.log(current) // 6
})
updateAndGet
Updates the value and returns the new value.const updateAndGet: {
<A>(f: (a: A) => A): (self: Ref<A>) => Effect.Effect<A>
<A>(self: Ref<A>, f: (a: A) => A): Effect.Effect<A>
}
import { Effect, Ref } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(10)
const updated = yield* Ref.updateAndGet(ref, n => n + 5)
console.log(updated) // 15
})
setAndGet
Sets a new value and returns it.const setAndGet: {
<A>(value: A): (self: Ref<A>) => Effect.Effect<A>
<A>(self: Ref<A>, value: A): Effect.Effect<A>
}
Conditional Updates
updateSome
Conditionally updates the value.const updateSome: {
<A>(f: (a: A) => Option.Option<A>): (self: Ref<A>) => Effect.Effect<void>
<A>(self: Ref<A>, f: (a: A) => Option.Option<A>): Effect.Effect<void>
}
import { Effect, Ref, Option } from "effect"
const program = Effect.gen(function* () {
const ref = yield* Ref.make(5)
// Only update if value is even
yield* Ref.updateSome(ref, n =>
n % 2 === 0 ? Option.some(n * 2) : Option.none()
)
const value = yield* Ref.get(ref)
console.log(value) // 5 (unchanged, 5 is odd)
})
getAndUpdateSome
const getAndUpdateSome: {
<A>(pf: (a: A) => Option.Option<A>): (self: Ref<A>) => Effect.Effect<A>
<A>(self: Ref<A>, pf: (a: A) => Option.Option<A>): Effect.Effect<A>
}
updateSomeAndGet
const updateSomeAndGet: {
<A>(pf: (a: A) => Option.Option<A>): (self: Ref<A>) => Effect.Effect<A>
<A>(self: Ref<A>, pf: (a: A) => Option.Option<A>): Effect.Effect<A>
}
modifySome
const modifySome: {
<B, A>(fallback: B, pf: (a: A) => Option.Option<readonly [B, A]>): (self: Ref<A>) => Effect.Effect<B>
<A, B>(self: Ref<A>, fallback: B, pf: (a: A) => Option.Option<readonly [B, A]>): Effect.Effect<B>
}
Concurrent Counter Example
import { Effect, Ref, Fiber } from "effect"
const program = Effect.gen(function* () {
const counter = yield* Ref.make(0)
// Increment counter concurrently from 10 fibers
const fibers = Array.from({ length: 10 }, () =>
Effect.fork(
Effect.gen(function* () {
for (let i = 0; i < 100; i++) {
yield* Ref.update(counter, n => n + 1)
}
})
)
)
const handles = yield* Effect.all(fibers)
yield* Fiber.joinAll(handles)
const total = yield* Ref.get(counter)
console.log(total) // 1000 (10 fibers * 100 increments)
})
Shared Cache Example
import { Effect, Ref, Option } from "effect"
interface Cache<K, V> {
get: (key: K) => Effect.Effect<Option.Option<V>>
set: (key: K, value: V) => Effect.Effect<void>
delete: (key: K) => Effect.Effect<void>
}
const makeCache = <K, V>(): Effect.Effect<Cache<K, V>> =>
Effect.gen(function* () {
const ref = yield* Ref.make<Map<K, V>>(new Map())
return {
get: (key: K) =>
Ref.modify(ref, map => {
const value = map.get(key)
return [Option.fromNullable(value), map]
}),
set: (key: K, value: V) =>
Ref.update(ref, map => new Map(map).set(key, value)),
delete: (key: K) =>
Ref.update(ref, map => {
const newMap = new Map(map)
newMap.delete(key)
return newMap
})
}
})
const program = Effect.gen(function* () {
const cache = yield* makeCache<string, number>()
yield* cache.set("a", 1)
yield* cache.set("b", 2)
const value = yield* cache.get("a")
console.log(value) // Option.some(1)
yield* cache.delete("a")
const deleted = yield* cache.get("a")
console.log(deleted) // Option.none()
})
State Machine Example
import { Effect, Ref, Match } from "effect"
type State =
| { _tag: "Idle" }
| { _tag: "Loading" }
| { _tag: "Success"; data: string }
| { _tag: "Error"; error: string }
const program = Effect.gen(function* () {
const state = yield* Ref.make<State>({ _tag: "Idle" })
const startLoading = Ref.set(state, { _tag: "Loading" })
const setSuccess = (data: string) => Ref.set(state, { _tag: "Success", data })
const setError = (error: string) => Ref.set(state, { _tag: "Error", error })
yield* startLoading
const result = yield* Effect.gen(function* () {
yield* Effect.sleep("100 millis")
return "Data loaded"
}).pipe(Effect.either)
if (result._tag === "Right") {
yield* setSuccess(result.right)
} else {
yield* setError(String(result.left))
}
const final = yield* Ref.get(state)
console.log(final) // { _tag: "Success", data: "Data loaded" }
})