Skip to content

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)
// => 0
  • 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-keepAlive atoms 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.

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.


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)
// => 5

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)))
// => 1

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

Type guard returning true when a value carries the AtomRegistry type id.

import { AtomRegistry } from "effect/unstable/reactivity"
AtomRegistry.isAtomRegistry(AtomRegistry.make()) // => true
AtomRegistry.isAtomRegistry({}) // => false

The literal type id "~effect/reactivity/AtomRegistry" used to brand registries.

import { AtomRegistry } from "effect/unstable/reactivity"
AtomRegistry.TypeId
// => "~effect/reactivity/AtomRegistry"

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.

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"

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)
// => 42

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 => 10
registry.get(count)
// => 11

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)
// => 5

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 recomputes

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: 1
unsubscribe() // stop listening; node may now be removed

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
// ...later
release()
function computeOnce() {
return 1
}

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.

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
// => 1

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)

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 disposed

registry.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)

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() // => 1
node.currentState() // => "valid"
node.children.length // => 0

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.

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 1

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"]

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 7

Writes 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 form

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

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

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>

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.