AtomRegistry
The AtomRegistry is the runtime cache and evaluator behind atoms.
A registry owns the node graph for a group of atoms: it stores current values,
records parent/child dependencies while atoms are read, and coordinates writes,
refreshes, subscriptions, stream conversions, and node disposal.
An Atom is just a recipe. It holds no value of its own — the
same atom object can have a different cached value in every registry it is read from.
This is the key to isolation: use a separate registry per UI root, per request, per
test, or per route boundary, and each gets its own independent state.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)const doubled = Atom.make((get) => get(count) * 2)
const registry = AtomRegistry.make()
registry.set(count, 21)registry.get(doubled)// => 42
// A second registry has completely independent state.const other = AtomRegistry.make()other.get(doubled)// => 0Mental model
Section titled “Mental model”- Reading an atom (
registry.get) creates or reuses a node, evaluates the atom when its value is missing or stale, and records any nested atom reads as dependencies. - Writing a writable atom (
set/modify/update) updates its node through the atom’s write function, invalidates dependents, and notifies listeners after batching settles. - Subscriptions (
subscribe) and scoped mounts (mount) keep nodes alive. When the last listener and dependent child disappear, non-keepAliveatoms are removed immediately or after their idle TTL. - Effects and streams started by atoms run with the registry scheduler and are finalized when their node is rebuilt, removed, reset, or disposed.
- Disposing a registry clears its nodes and makes later atom access an error.
Reading and writing from Effect
Section titled “Reading and writing from Effect”In real applications you usually provide a registry to your Effect program with a
layer and read or write atoms with the module-level converters, which
require the AtomRegistry service. Use Effect.gen for imperative flows.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"import { Effect } from "effect"
const count = Atom.make(0)const doubled = Atom.make((get) => get(count) * 2)
const program = Effect.gen(function* () { yield* Atom.set(count, 10) const value = yield* Atom.get(doubled) return value // => 20})
// Provide a fresh, isolated registry, disposed when the scope closes.const result = program.pipe( Effect.provide(AtomRegistry.layer), Effect.scoped)When an atom produces an AsyncResult (e.g. it wraps an Effect),
use getResult to await its first non-Initial value as a normal
Effect, or toStreamResult to observe successive values as a
Stream.
Reference
Section titled “Reference”Creates a registry directly. Options preload initial atom values, supply a custom task
scheduler, tune timeoutResolution (the bucket size for idle sweeps, default 1000ms),
and set a defaultIdleTTL applied to atoms without their own TTL.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)
const registry = AtomRegistry.make({ initialValues: [[count, 5]], defaultIdleTTL: 30_000 // ms; unobserved atoms linger 30s before removal})
registry.get(count)// => 5AtomRegistry (service tag)
Section titled “AtomRegistry (service tag)”The Context.Service tag for the active runtime cache. Use it to access the registry
inside an Effect, or as the requirement satisfied by layer.
import { AtomRegistry } from "effect/unstable/reactivity"import { Effect } from "effect"
const nodeCount = Effect.gen(function* () { const registry = yield* AtomRegistry.AtomRegistry return registry.getNodes().size})// Effect<number, never, AtomRegistry>The default layer providing a fresh AtomRegistry. The registry is disposed when the
layer’s scope is finalized.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"import { Effect } from "effect"
const counter = Atom.make(0)
const program = Atom.update(counter, (n) => n + 1).pipe( Effect.andThen(Atom.get(counter)))
Effect.runPromise(program.pipe(Effect.provide(AtomRegistry.layer)))// => 1layerOptions
Section titled “layerOptions”Like layer but lets you configure the underlying make options
(initial values, scheduler, idle TTL).
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)
const TestRegistry = AtomRegistry.layerOptions({ initialValues: [[count, 100]], defaultIdleTTL: 0 // disable idle removal; combine with keepAlive for full control})isAtomRegistry
Section titled “isAtomRegistry”Type guard returning true when a value carries the AtomRegistry type id.
import { AtomRegistry } from "effect/unstable/reactivity"
AtomRegistry.isAtomRegistry(AtomRegistry.make()) // => trueAtomRegistry.isAtomRegistry({}) // => falseTypeId
Section titled “TypeId”The literal type id "~effect/reactivity/AtomRegistry" used to brand registries.
import { AtomRegistry } from "effect/unstable/reactivity"
AtomRegistry.TypeId// => "~effect/reactivity/AtomRegistry"The AtomRegistry interface
Section titled “The AtomRegistry interface”The registry instance exposes synchronous methods for direct, non-Effect use (for example from imperative UI glue). The same operations are available as Effect-returning converters described in the next section.
registry.get
Section titled “registry.get”Reads an atom’s current value, creating or rebuilding its node if needed.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const greeting = Atom.make("hello")const registry = AtomRegistry.make()
registry.get(greeting)// => "hello"registry.set
Section titled “registry.set”Writes a value to a Writable atom, invalidating its dependents.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)const registry = AtomRegistry.make()
registry.set(count, 42)registry.get(count)// => 42registry.modify
Section titled “registry.modify”Reads the current value, computes [returnValue, nextValue], writes nextValue, and
returns returnValue. Useful for an atomic read-then-write.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(10)const registry = AtomRegistry.make()
const previous = registry.modify(count, (n) => [n, n + 1])// previous => 10registry.get(count)// => 11registry.update
Section titled “registry.update”Writes the value returned by a function of the current value.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)const registry = AtomRegistry.make()
registry.update(count, (n) => n + 5)registry.get(count)// => 5registry.refresh
Section titled “registry.refresh”Re-runs an atom’s refresh logic (or invalidates it when it has none), forcing recomputation on the next read — e.g. to re-fetch a derived/async atom.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const id = Atom.make(1)const user = Atom.make((get) => `user-${get(id)}`)const registry = AtomRegistry.make()
registry.get(user) // => "user-1"registry.refresh(user) // marks the node stale; next get recomputesregistry.subscribe
Section titled “registry.subscribe”Registers a listener for value changes and returns a release callback. Pass
{ immediate: true } to invoke the listener once with the current value right away.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)const registry = AtomRegistry.make()
const unsubscribe = registry.subscribe( count, (value) => console.log("count:", value), { immediate: true })// logs: count: 0
registry.set(count, 1) // logs: count: 1unsubscribe() // stop listening; node may now be removedregistry.mount
Section titled “registry.mount”Keeps an atom alive with a no-op listener and returns a release callback. Equivalent to
subscribe(atom, () => {}, { immediate: true }). Prefer the scoped
mount helper inside Effect code.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const expensive = Atom.make((get) => computeOnce())const registry = AtomRegistry.make()
const release = registry.mount(expensive) // node stays alive// ...laterrelease()
function computeOnce() { return 1}registry.setSerializable
Section titled “registry.setSerializable”Preloads an encoded value for a serializable atom’s stable key, so the node hydrates from it on first read (e.g. SSR/state transfer). The encoded value is decoded by the matching serializable atom.
import { AtomRegistry } from "effect/unstable/reactivity"
const registry = AtomRegistry.make()registry.setSerializable("user/profile", { name: "Ada" })// Consumed when an atom with serialization key "user/profile" is first read.registry.getNodes
Section titled “registry.getNodes”Returns the live ReadonlyMap of nodes keyed by atom (or serialization key). Mostly
useful for debugging and devtools.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const a = Atom.make(0)const registry = AtomRegistry.make()registry.get(a)
registry.getNodes().size// => 1registry.reset
Section titled “registry.reset”Removes every node, running their finalizers and clearing idle timers. The registry remains usable; subsequent reads rebuild from scratch.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const a = Atom.make(0)const registry = AtomRegistry.make()registry.set(a, 9)registry.reset()registry.get(a)// => 0 (rebuilt)registry.dispose
Section titled “registry.dispose”Resets the registry and marks it disposed: any later atom access throws. Use a fresh
registry when a whole lifetime should start from empty state. Provided automatically by
layer on scope close.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const a = Atom.make(0)const registry = AtomRegistry.make()registry.dispose()// registry.get(a) // throws: registry is disposedregistry.scheduler / registry.schedulerAsync
Section titled “registry.scheduler / registry.schedulerAsync”The Scheduler instances driving synchronous and asynchronous tasks (effect execution,
deferred node removal). You rarely touch these directly; they exist for integration and
advanced control.
import { AtomRegistry } from "effect/unstable/reactivity"
const registry = AtomRegistry.make()registry.scheduler // Scheduler (sync)registry.schedulerAsync // Scheduler (async)registry.onNodeAdded / registry.onNodeRemoved
Section titled “registry.onNodeAdded / registry.onNodeRemoved”Optional callbacks invoked when a node is added to or removed from the registry. Handy for devtools, logging, or leak detection.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const registry = AtomRegistry.make()registry.onNodeAdded = (node) => console.log("added", node.currentState())registry.onNodeRemoved = (node) => console.log("removed")
registry.get(Atom.make(0)) // logs: added uninitialized (the node fires before its first read)Node<A>
Section titled “Node<A>”A registry node for a single atom. It exposes the originating atom, a value()
accessor, the parents/children dependency links, the listeners set, and a
currentState() returning "uninitialized" | "stale" | "valid" | "removed".
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const a = Atom.make(1)const registry = AtomRegistry.make()registry.get(a)
const node = registry.getNodes().get(a)!node.value() // => 1node.currentState() // => "valid"node.children.length // => 0Converters: the Effect-facing API
Section titled “Converters: the Effect-facing API”These module-level functions read or observe atoms through the AtomRegistry service,
so they return Effects/Streams with AtomRegistry in their requirements. They are the
recommended way to touch atom state from Effect.gen. The same functions are also
re-exported on the Atom module (Atom.get, Atom.set, …) for ergonomic use.
toStream
Section titled “toStream”Converts any atom into a Stream that emits the current value immediately, then every
subsequent change, unsubscribing when the stream scope closes.
The AtomRegistry.toStream form is dual: call it on a registry value
(AtomRegistry.toStream(registry, atom)) or pipe a registry into it. The Atom-module
form (Atom.toStream) needs no registry argument because it pulls one from the service:
import { Atom, AtomRegistry } from "effect/unstable/reactivity"import { Effect, Stream } from "effect"
const count = Atom.make(0)
const program = Effect.gen(function* () { yield* Atom.toStream(count).pipe( Stream.take(2), Stream.runForEach((n) => Effect.sync(() => console.log(n))), Effect.forkScoped ) yield* Atom.set(count, 1)})// logs: 0 then 1toStreamResult
Section titled “toStreamResult”Converts an AsyncResult atom into a Stream of successful values: Initial
results are skipped, failures fail the stream with their cause, and duplicates are
dropped via Stream.changes.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"import { Effect, Stream } from "effect"
const data = Atom.make(Effect.succeed("ready")) // Atom<AsyncResult<string>>
const values = Effect.gen(function* () { return yield* Atom.toStreamResult(data).pipe(Stream.take(1), Stream.runCollect)})// => ["ready"]getResult
Section titled “getResult”Reads an AsyncResult atom as an Effect, waiting until the result leaves Initial
(and through waiting results when suspendOnWaiting: true). Succeeds with the value
or fails with the result’s error.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"import { Effect } from "effect"
const user = Atom.make(Effect.succeed({ name: "Ada" }))
const program = Effect.gen(function* () { const value = yield* Atom.getResult(user) return value.name // => "Ada"})
Effect.runPromise(program.pipe(Effect.provide(AtomRegistry.layer), Effect.scoped))Reads an atom’s current value as an Effect.
import { Atom } from "effect/unstable/reactivity"
const count = Atom.make(7)
Atom.get(count)// Effect<number, never, AtomRegistry> — yields 7Writes a value to a writable atom through the registry service. Dual, so it pipes too.
import { Atom } from "effect/unstable/reactivity"
const count = Atom.make(0)
Atom.set(count, 5)// Effect<void, never, AtomRegistry>
count.pipe(Atom.set(5)) // equivalent, piped formupdate
Section titled “update”Writes the result of applying a function to the current value.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const count = Atom.make(0)
const program = Effect.gen(function* () { yield* Atom.update(count, (n) => n + 1) return yield* Atom.get(count) // => 1})modify
Section titled “modify”Atomically reads, computes [returnValue, nextValue], writes nextValue, and yields
returnValue.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const count = Atom.make(10)
const program = Effect.gen(function* () { const prev = yield* Atom.modify(count, (n) => [n, n + 1]) return prev // => 10, count is now 11})refresh
Section titled “refresh”Requests a refresh of an atom through the service, invalidating it so the next read recomputes.
import { Atom } from "effect/unstable/reactivity"
const data = Atom.make((get) => Date.now())
Atom.refresh(data)// Effect<void, never, AtomRegistry>Mounts an atom for the lifetime of the current Effect Scope: it subscribes with a
no-op listener and releases automatically when the scope finalizes. This keeps an
atom (and its effects/streams) alive without an explicit unsubscribe.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const polling = Atom.make((get) => 0) // imagine a recurring source
const program = Effect.gen(function* () { yield* Atom.mount(polling) // stays alive until the scope closes // ... do work while the atom is kept warm})// Effect<void, never, AtomRegistry | Scope>Batching internals
Section titled “Batching internals”When multiple writes happen inside a single tick, the registry batches
invalidation: it collects stale nodes, rebuilds them, then notifies listeners once,
so dependents don’t observe intermediate states. The relevant symbols — batch,
BatchPhase, and batchState — are exported but marked @internal; they back the
runtime’s coalescing and are not part of the stable public API. You normally never call
them directly. For how invalidation and node removal flow, see
Invalidation & lifecycle.