Atom
An Atom<A> is a declaration of how to read a value. It does not hold state
on its own — the AtomRegistry is the runtime
owner that evaluates the read, caches the result, records dependency edges, runs
effects and streams, and disposes nodes that are no longer observed.
This page is about declaring atoms: synchronous state, effect- and
stream-backed values exposed as AsyncResult,
command-style functions, pull-based streams, runtime-backed atoms, and
parameterized families. For reading and writing atoms from Effect code (get,
set, update, refresh, mount) see the
Registry page, and for deriving / transforming
atoms (map, transform, swr, debounce, keepAlive, …) see
Atom combinators.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
// Writable state atom — created at module scope so its identity is stable.const count = Atom.make(0)
// Derived atom: reading `get(count)` builds a dependency edge.const doubled = Atom.make((get) => get(count) * 2)
const registry = AtomRegistry.make()
registry.get(count) // => 0registry.get(doubled) // => 0
registry.set(count, 5)registry.get(doubled) // => 10 (recomputed because `count` changed)The mental model
Section titled “The mental model”Three rules explain almost all atom behavior:
- A regular
get(atom)inside a read function creates a dependency. When that dependency changes or is refreshed, every atom that read it is invalidated and recomputed on the next read. Useget.once(atom)to read the current value without creating an edge. - Atom identity is the cache key. The same atom object can hold different
cached values in different registries. So define atoms once at module scope.
For atoms parameterized by an input value, use
family— never build a fresh atom inline on every render. - Cache lifetime belongs to the registry, not the atom. An unobserved atom
that is not
keepAlivemay be disposed immediately (or after itsidleTTL), which releases finalizers and rebuilds effects, streams, and derived state on the next read. Reading anEffect-backed atom does not by itself keep external data subscribed.
The four kinds of read
Section titled “The four kinds of read”Atom.make is overloaded to cover the common cases. What you pass decides the
atom’s value type:
import { Atom } from "effect/unstable/reactivity"import { Effect, Stream } from "effect"
// 1. A plain value -> a Writable<A> state atom.const name = Atom.make("Alice") // Writable<string>
// 2. A read function -> a derived Atom<A> (sync).const greeting = Atom.make((get) => `Hello, ${get(name)}`) // Atom<string>
// 3. An Effect -> Atom<AsyncResult<A, E>>.const user = Atom.make( Effect.succeed({ id: 1, name: "Alice" })) // Atom<AsyncResult<{ id: number; name: string }, never>>
// 4. A Stream -> Atom<AsyncResult<A, E | NoSuchElementError>>.const ticks = Atom.make( Stream.tick("1 second").pipe(Stream.as(1))) // Atom<AsyncResult<number, NoSuchElementError>>For effects and streams you can also pass a function (get) => Effect | Stream
so the work can depend on other atoms:
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const userId = Atom.make(1)
const user = Atom.make((get) => Effect.succeed({ id: get(userId), name: "Alice" }))// When `userId` changes, the effect re-runs and the AsyncResult transitions// through waiting -> success.Service / Layer style with a runtime
Section titled “Service / Layer style with a runtime”Real applications run effects that need services. Wrap a Layer in an
AtomRuntime and use its atom / fn constructors; every effect
read by those atoms is provided the layer’s context.
import { Atom } from "effect/unstable/reactivity"import { Context, Effect, Layer } from "effect"
class Users extends Context.Service<Users>()("app/Users", { succeed: { byId: (id: number) => Effect.succeed({ id, name: `user-${id}` }) }}) {}
// A runtime atom built from a Layer (shares a MemoMap across the app).const runtime = Atom.runtime(Users.layer)
const userId = Atom.make(1)
// `runtime.atom` provides `Users` to the effect automatically.const user = runtime.atom((get) => Effect.gen(function* () { const users = yield* Users return yield* users.byId(get(userId)) }))// => Atom<AsyncResult<{ id: number; name: string }, never>>Reference
Section titled “Reference”Models and guards
Section titled “Models and guards”Atom<A>
Section titled “Atom<A>”The core interface. An atom carries a read function plus caching metadata. All
fields except read are optional in spirit and usually set via
combinators rather than by hand.
import { Atom } from "effect/unstable/reactivity"
interface Atom<A> { readonly read: (get: Atom.AtomContext) => A readonly keepAlive: boolean // stay cached with no observers? readonly lazy: boolean // defer recompute while unobserved? readonly refresh?: (f: <A>(atom: Atom<A>) => void) => void readonly label?: readonly [name: string, stack: string] readonly idleTTL?: number // ms to keep alive after going idle readonly initialValueTarget?: Atom<A>}
const a = Atom.make(1)a.keepAlive // => falsea.lazy // => trueisAtom
Section titled “isAtom”Type guard for any Atom.
import { Atom } from "effect/unstable/reactivity"
Atom.isAtom(Atom.make(1)) // => trueAtom.isAtom(42) // => falseWritable<R, W>
Section titled “Writable<R, W>”An atom that can also be written to. R is the read type, W the write input
type (often R, but e.g. command atoms accept different write inputs). Carries a
write(ctx, value) function in addition to read.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0) // Writable<number, number>const registry = AtomRegistry.make()registry.set(count, 3)registry.get(count) // => 3isWritable
Section titled “isWritable”Type guard narrowing an Atom<R> to Writable<R, W>.
import { Atom } from "effect/unstable/reactivity"
Atom.isWritable(Atom.make(0)) // => trueAtom.isWritable(Atom.make((get) => 1)) // => false (read-only derived atom)TypeId / WritableTypeId
Section titled “TypeId / WritableTypeId”The runtime brand strings used by the guards above.
import { Atom } from "effect/unstable/reactivity"
Atom.TypeId // => "~effect/reactivity/Atom"Atom.WritableTypeId // => "~effect/reactivity/Atom/Writable"Utility types
Section titled “Utility types”Type-level helpers for extracting an atom’s value shape.
import { Atom } from "effect/unstable/reactivity"import type { AsyncResult } from "effect/unstable/reactivity"
declare const a: Atom.Atom<number>declare const r: Atom.Atom<AsyncResult.AsyncResult<string, Error>>
type T = Atom.Type<typeof a> // => numbertype S = Atom.Success<typeof r> // => stringtype F = Atom.Failure<typeof r> // => Error
// Atom.PullSuccess<T> extracts the item type of a `pull` atom's PullResult.// Atom.WithoutSerializable<T> strips serializable metadata, keeping// Writable read/write types when the source is writable.Context passed to read / write functions
Section titled “Context passed to read / write functions”AtomContext
Section titled “AtomContext”The get callback passed to every read function. Calling it as a function
(get(atom)) reads a dependency; it also has methods for one-shot reads,
awaiting AsyncResult/Option values, subscriptions, finalizers, and writing
back to itself or other atoms.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const source = Atom.make(1)
const derived = Atom.make((get) => { get(source) // dependency edge: recompute when `source` changes get.once(source) // read current value, NO dependency edge get.self<number>() // previous value of THIS atom, as Option get.addFinalizer(() => { /* runs when this atom is disposed */ }) return get.once(source) + 1})Notable AtomContext members:
get(atom)/get.get(atom)— read with a dependency edge.get.once(atom)— read current value, no edge.get.result(asyncResultAtom, { suspendOnWaiting? })— await anAsyncResultatom as anEffect<A, E>(with edge);get.resultOnce(...)without an edge.get.some(optionAtom)/get.someOnce(...)— await anAtom<Option<A>>as anEffect<A>, suspending until the option isSome(get.someadds an edge,get.someOncedoes not).get.self<A>()— this atom’s current cached value asOption<A>.get.setSelf(a)— set this atom’s value (used by async read functions).get.refreshSelf()/get.refresh(atom)— request recomputation.get.set(writable, value)/get.setResult(writable, value)— write to another writable atom.get.subscribe(atom, f, { immediate? })— observe changes via a callback.get.stream(atom, opts)/get.streamResult(asyncResultAtom, opts)— observe as aStream.get.mount(atom)— keep an atom mounted for this read’s lifetime.get.addFinalizer(f)— run cleanup when this atom is disposed.get.registry— the owningAtomRegistry.
WriteContext<A>
Section titled “WriteContext<A>”The context passed to a writable atom’s write function. It is intentionally
smaller than AtomContext: read other atoms, refresh or set the current atom,
and write to other writables.
import { Atom } from "effect/unstable/reactivity"
const log = Atom.make<Array<string>>([])
const command = Atom.writable( () => null, (ctx, message: string) => { ctx.get(log) // read another atom ctx.set(log, [...ctx.get(log), message]) // write another writable ctx.setSelf(null) // set this atom ctx.refreshSelf() // request recompute })FnContext
Section titled “FnContext”The context passed to fn and fnSync computations. Like
AtomContext but tailored for command atoms (it has result, setResult,
some, stream, streamResult, subscribe, self, setSelf, set,
refresh, mount, addFinalizer, and registry), but it omits the edge-free
one-shot reads once, resultOnce, and someOnce. See fn below for usage.
Constructors
Section titled “Constructors”readable
Section titled “readable”The low-level read-only atom constructor: a read function and an optional
refresh registration callback. Most code uses make instead.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const now = Atom.readable(() => 42)AtomRegistry.make().get(now) // => 42writable
Section titled “writable”The low-level writable constructor: read and write functions (plus optional
refresh). The building block behind make(value), fn, subscriptionRef, etc.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"import { Option } from "effect"
// A clamped counter: writes are stored via setSelf inside `write`.const clamped = Atom.writable<number, number>( (get) => Option.getOrElse(get.self<number>(), () => 0), (ctx, value) => ctx.setSelf(Math.max(0, Math.min(10, value))))
const registry = AtomRegistry.make()registry.set(clamped, 99)registry.get(clamped) // => 10The primary, overloaded constructor. Dispatches on what you pass:
| You pass | You get |
|---|---|
a plain value A | Writable<A> (state atom) |
(get) => A | Atom<A> (derived sync) |
Effect<A, E, ...> or (get) => Effect<...> | Atom<AsyncResult<A, E>> |
Stream<A, E, ...> or (get) => Stream<...> | Atom<AsyncResult<A, E | NoSuchElementError>> |
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
Atom.make(0) // => Writable<number>Atom.make((get) => get(Atom.make(0)) + 1) // => Atom<number>Atom.make(Effect.succeed("hi")) // => Atom<AsyncResult<string, never>>
// Effect/Stream options: seed an initial value, or run uninterruptibly.Atom.make(Effect.succeed(1), { initialValue: 0, uninterruptible: true })subscriptionRef
Section titled “subscriptionRef”Exposes a SubscriptionRef (or an effect producing one) as a writable atom.
Reading observes ref changes; writing sets the ref. When given a bare ref the
atom value is A; when given an effect it is AsyncResult<A, E>.
import { Atom } from "effect/unstable/reactivity"import { Effect, SubscriptionRef } from "effect"import { AtomRegistry } from "effect/unstable/reactivity"
const program = Effect.gen(function* () { const ref = yield* SubscriptionRef.make(0)
const atom = Atom.subscriptionRef(ref) // Writable<number> const registry = AtomRegistry.make()
registry.get(atom) // => 0 yield* SubscriptionRef.set(ref, 5) registry.get(atom) // => 5 (atom tracks the ref)})fnSync
Section titled “fnSync”A synchronous command atom: a writable whose value is the result of the last
call. Writing an argument re-runs the function. Without an initialValue the read
type is Option<A> (None before the first call); with one it is A.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const slugify = Atom.fnSync((title: string) => title.toLowerCase().replace(/\s+/g, "-"))
const registry = AtomRegistry.make()registry.get(slugify) // => Option.none()registry.set(slugify, "Hello World")registry.get(slugify) // => Option.some("hello-world")
// With initialValue the result type is the bare value:const counter = Atom.fnSync( (n: number, get) => n * 2, { initialValue: 0 })An effectful command atom. Writing an argument starts an Effect (or
Stream) and the atom’s value is its AsyncResult. The result type is
AtomResultFn<Arg, A, E>. Writes also accept the Reset
and Interrupt control symbols.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"import { AtomRegistry } from "effect/unstable/reactivity"
const search = Atom.fn((query: string) => Effect.succeed([`result for ${query}`]))// => AtomResultFn<string, string[], never>
const registry = AtomRegistry.make()registry.get(search) // => AsyncResult.Initial (no call yet)registry.set(search, "effect") // starts the effect -> waiting -> successregistry.set(search, Atom.Reset) // back to Initialregistry.set(search, Atom.Interrupt) // interrupt the running effect
// `concurrent: true` keeps overlapping calls running and joins them;// otherwise a new write interrupts the previous one.const upload = Atom.fn( (file: string) => Effect.succeed(file), { concurrent: true })When you need the argument generic fixed first (e.g. for a typed write input),
call fn<Arg>() with no other args to get a curried constructor:
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const byId = Atom.fn<number>()((id, get) => Effect.succeed({ id }))// => AtomResultFn<number, { id: number }, never>A pull-based stream atom. The first read pulls the initial chunk; each write
(void) pulls the next chunk. Items accumulate by default into a
PullResult, unless disableAccumulation is set. Useful for
“load more” / infinite-scroll style streams.
import { Atom } from "effect/unstable/reactivity"import { Stream } from "effect"import { AtomRegistry } from "effect/unstable/reactivity"
const pages = Atom.pull(Stream.fromIterable([1, 2, 3, 4]))// => Writable<PullResult<number>, void>
const registry = AtomRegistry.make()registry.get(pages) // pulls first chunk -> AsyncResult success { done, items }registry.set(pages, undefined) // pull the next chunk
// Don't accumulate — each pull replaces the items.Atom.pull(Stream.fromIterable([1, 2, 3]), { disableAccumulation: true })family
Section titled “family”Memoizes an atom factory so the same input returns the same atom object — giving parameterized atoms a stable identity (and therefore a stable cache key). Uses weak references where the platform supports them, so unused entries can be collected.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
// One atom per user id, created on demand and cached by id.const userById = Atom.family((id: number) => Atom.make(Effect.succeed({ id, name: `user-${id}` })))
userById(1) === userById(1) // => true (same atom object)userById(1) === userById(2) // => falsecontext
Section titled “context”Creates a RuntimeFactory backed by a supplied
Layer.MemoMap. Use this when you want runtime atoms to share layer memoization
with a specific memo map.
import { Atom } from "effect/unstable/reactivity"import { Layer } from "effect"
const factory = Atom.context({ memoMap: Layer.makeMemoMapUnsafe() })// `factory(layer)` then produces AtomRuntime values sharing that MemoMap.defaultMemoMap
Section titled “defaultMemoMap”The default Layer.MemoMap used by the module-level runtime
factory.
import { Atom } from "effect/unstable/reactivity"
Atom.defaultMemoMap // => Layer.MemoMap (shared by Atom.runtime)runtime
Section titled “runtime”The default RuntimeFactory, built on defaultMemoMap. Call
it with a Layer to get an AtomRuntime.
import { Atom } from "effect/unstable/reactivity"import { Context, Effect } from "effect"
class Config extends Context.Service<Config>()("app/Config", { succeed: { apiUrl: "https://example.com" }}) {}
const runtime = Atom.runtime(Config.layer)
const apiUrl = runtime.atom( Effect.map(Config, (c) => c.apiUrl))// => Atom<AsyncResult<string, never>>withReactivity
Section titled “withReactivity”runtime.withReactivity(keys) — refresh an atom whenever the given invalidation
keys change in the default Reactivity runtime. (See
Reactivity for keys and invalidation.)
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const todos = Atom.make(Effect.succeed([])).pipe( Atom.withReactivity(["todos"]))// Refreshes whenever the "todos" key is invalidated.Runtime models
Section titled “Runtime models”AtomRuntime
Section titled “AtomRuntime”The atom returned by a RuntimeFactory. Its value is the built
Context, and it exposes context-aware constructors: runtime.atom,
runtime.fn, runtime.pull, and runtime.subscriptionRef. Each runs its
effect/stream with the runtime’s services provided, so reads can use R and fail
with the layer’s error ER.
import { Atom } from "effect/unstable/reactivity"import { Context, Effect } from "effect"
class Db extends Context.Service<Db>()("app/Db", { succeed: { query: (sql: string) => Effect.succeed([sql]) }}) {}
const runtime = Atom.runtime(Db.layer)
const rows = runtime.atom((get) => Effect.flatMap(Db, (db) => db.query("select 1")))
const mutate = runtime.fn((sql: string, get) => Effect.flatMap(Db, (db) => db.query(sql)))
runtime.layer // => Atom<Layer<Db, never>> (the underlying layer atom)RuntimeFactory
Section titled “RuntimeFactory”A callable that turns a Layer (or (get) => Layer) into an AtomRuntime,
sharing a Layer.MemoMap. Also exposes memoMap, addGlobalLayer (merge a layer
into every runtime it produces), and withReactivity.
import { Atom } from "effect/unstable/reactivity"import { Layer } from "effect"
const factory = Atom.runtimefactory.memoMap // => Layer.MemoMapfactory.addGlobalLayer(Layer.empty) // merged into all runtimes from this factoryCommand-atom models and control symbols
Section titled “Command-atom models and control symbols”AtomResultFn
Section titled “AtomResultFn”The type returned by fn: a Writable<AsyncResult<A, E>, Arg | Reset | Interrupt>.
You read its AsyncResult and write arguments (or control symbols) to it.
import type { Atom } from "effect/unstable/reactivity"
// AtomResultFn<Arg, A, E> ≈ Writable<AsyncResult<A, E>, Arg | Reset | Interrupt>declare const f: Atom.AtomResultFn<string, number, Error>Control symbol written to an AtomResultFn to clear it back to the Initial
state.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"import { AtomRegistry } from "effect/unstable/reactivity"
const fn = Atom.fn((n: number) => Effect.succeed(n))const registry = AtomRegistry.make()registry.set(fn, 1) // successregistry.set(fn, Atom.Reset) // => AsyncResult.Initial againInterrupt
Section titled “Interrupt”Control symbol written to an AtomResultFn to interrupt the currently running
computation.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"import { AtomRegistry } from "effect/unstable/reactivity"
const fn = Atom.fn((ms: number) => Effect.sleep(ms).pipe(Effect.as(ms)))const registry = AtomRegistry.make()registry.set(fn, 10000) // starts a long-running effect (waiting)registry.set(fn, Atom.Interrupt) // => Failure with an interrupt causePullResult
Section titled “PullResult”The AsyncResult produced by a pull atom: a success carrying a
non-empty items batch and a done flag, or a NoSuchElementError failure when
the stream completes with no items.
import type { Atom } from "effect/unstable/reactivity"
// PullResult<A, E> = AsyncResult<{ done: boolean; items: NonEmptyArray<A> },// E | NoSuchElementError>declare const r: Atom.PullResult<number>Per-atom metadata helpers
Section titled “Per-atom metadata helpers”These return a copy of an atom with adjusted lifetime/identity metadata. They live in the same module but pair naturally with construction; the full set of derivation combinators is on the Atom combinators page.
setIdleTTL
Section titled “setIdleTTL”Disposes an atom only after it has been idle (unobserved) for the given duration;
an infinite duration is equivalent to keepAlive.
import { Atom } from "effect/unstable/reactivity"
const cached = Atom.make(0).pipe(Atom.setIdleTTL("30 seconds"))cached.idleTTL // => 30000initialValue
Section titled “initialValue”Pairs an atom with a preload value, producing a [Atom<A>, A] tuple you can hand
to a registry as a seed.
import { Atom, AtomRegistry } from "effect/unstable/reactivity"
const count = Atom.make(0)const seed = Atom.initialValue(count, 10) // => [count, 10]
const registry = AtomRegistry.make({ initialValues: [seed] })registry.get(count) // => 10Reading and writing from Effect code
Section titled “Reading and writing from Effect code”To read or mutate atoms from inside an Effect, use the registry-aware helpers
exported by this module — Atom.get, Atom.set, Atom.update, Atom.modify,
Atom.getResult, Atom.refresh, Atom.mount, Atom.toStream, and
Atom.toStreamResult. They all require the AtomRegistry service and are
documented on the Atom Registry page.
import { Atom } from "effect/unstable/reactivity"import { Effect } from "effect"
const count = Atom.make(0)
const program = Effect.gen(function* () { const current = yield* Atom.get(count) // requires AtomRegistry yield* Atom.set(count, current + 1) yield* Atom.update(count, (n) => n * 2)})