Skip to content

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) // => 0
registry.get(doubled) // => 0
registry.set(count, 5)
registry.get(doubled) // => 10 (recomputed because `count` changed)

Three rules explain almost all atom behavior:

  1. 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. Use get.once(atom) to read the current value without creating an edge.
  2. 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.
  3. Cache lifetime belongs to the registry, not the atom. An unobserved atom that is not keepAlive may be disposed immediately (or after its idleTTL), which releases finalizers and rebuilds effects, streams, and derived state on the next read. Reading an Effect-backed atom does not by itself keep external data subscribed.

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.

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

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 // => false
a.lazy // => true

Type guard for any Atom.

import { Atom } from "effect/unstable/reactivity"
Atom.isAtom(Atom.make(1)) // => true
Atom.isAtom(42) // => false

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

Type guard narrowing an Atom<R> to Writable<R, W>.

import { Atom } from "effect/unstable/reactivity"
Atom.isWritable(Atom.make(0)) // => true
Atom.isWritable(Atom.make((get) => 1)) // => false (read-only derived atom)

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"

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> // => number
type S = Atom.Success<typeof r> // => string
type 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.

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 an AsyncResult atom as an Effect<A, E> (with edge); get.resultOnce(...) without an edge.
  • get.some(optionAtom) / get.someOnce(...) — await an Atom<Option<A>> as an Effect<A>, suspending until the option is Some (get.some adds an edge, get.someOnce does not).
  • get.self<A>() — this atom’s current cached value as Option<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 a Stream.
  • 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 owning AtomRegistry.

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

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.

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

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

The primary, overloaded constructor. Dispatches on what you pass:

You passYou get
a plain value AWritable<A> (state atom)
(get) => AAtom<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 })

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

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 -> success
registry.set(search, Atom.Reset) // back to Initial
registry.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 })

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

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.

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)

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

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.

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)

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.runtime
factory.memoMap // => Layer.MemoMap
factory.addGlobalLayer(Layer.empty) // merged into all runtimes from this factory

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) // success
registry.set(fn, Atom.Reset) // => AsyncResult.Initial again

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 cause

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>

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.

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

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

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