name: effect-atom-integration description: Atom.runtime, Atom.make, runtimeAtom.fn, operation atoms, Result handling. Atom-as-State doctrine for integrating Effect services with React. model_invoked: true triggers:
- "Atom.runtime"
- "Atom.make"
- "effect-atom"
- "runtimeAtom"
- "operation atom"
- "Result"
Effect-Atom Integration
Overview
effect-atom bridges Effect-TS services with React via reactive atoms. This is the canonical state management pattern for TMNL.
CRITICAL DOCTRINE: Atom-as-State
From .edin/EFFECT_PATTERNS.md:
NO EFFECT.REF. EVER.
When React is the consumer via effect-atom,
Atom.make()is the primary state mechanism—notEffect.Refinside services.
- Service methods mutate Atoms directly (
Atom.set,ctx.set)- React subscribes directly to atoms
- This eliminates the Ref→Atom bridge: no polling, no SubscriptionRef, no streams-to-consume-streams
Canonical Sources
effect-atom Core
- Submodule:
../../submodules/effect-atom/packages/atom/src/Atom.ts:370-391— Atom.make (Effect detection)Atom.ts:458-463— Writable atomsAtom.ts:328-338— Readable (derived) atomsAtom.ts:643-715— Atom.runtimeAtom.ts:553-588— AtomRuntime.fn (operation atoms)Atom.ts:1316-1351— Atom.family
effect-atom Tests
- Submodule:
../../submodules/effect-atom/packages/atom/test/Atom.test.ts— Canonical usage patternsResult.test.ts— Result type handling
TMNL Battle-tested Implementations
- DataManager atoms —
src/lib/data-manager/v1/atoms/index.ts(canonical Atom-as-State) - Slider atoms —
src/lib/slider/v1/atoms/index.ts(runtime factories) - Testbed —
src/components/testbed/EffectAtomTestbed.tsx(comprehensive examples)
Patterns
Decision Tree: Which Atom Pattern?
Need reactive state?
│
├─ Simple mutable state (counter, toggle)?
│ └─ Use: Atom.make(value) → Writable<A>
│ Read: get(atom), Write: ctx.set(atom, value)
│
├─ Derived from other atoms (computed value)?
│ └─ Use: Atom.make((get) => ...) → Atom<A> (readonly)
│ Auto-tracks dependencies
│
├─ Async data (API call, database query)?
│ └─ Use: Atom.make(Effect) → Atom<Result<A, E>>
│ Result.isSuccess, Result.isFailure, Result.isInitial
│
├─ Service access (yield* MyService)?
│ └─ Use: Atom.runtime(Layer) → AtomRuntime<R, E>
│ Create atoms with service dependencies
│
├─ Mutation/action (searchOp, clearOp)?
│ └─ Use: runtime.fn<Arg>()((arg, ctx) => Effect)
│ Trigger with useSetAtom(op)(arg)
│
├─ Stream data (progressive, ticking)?
│ └─ Use: Atom.make(Stream) → Atom<Result<A, E>>
│ Updates progressively as stream emits
│
└─ Keyed atoms (layer-123, user-456)?
└─ Use: Atom.family((key) => Atom.make(...))
Stable references via WeakRef
Pattern 1: Atom.make — PRIMITIVE ATOMS
When to use:
- Simple mutable state
- UI toggles, counters, input bindings
- State that doesn't derive from async sources
Signature:
Atom.make<A>(initialValue: A): Atom.Writable<A>
Full Example:
import { Atom } from '@effect-atom/atom-react'
import { useAtomValue, useSetAtom } from '@effect-rx/rx-react'
// Module-level atoms (stable references)
const countAtom = Atom.make(0)
const nameAtom = Atom.make('')
const isOpenAtom = Atom.make(false)
// React component
function Counter() {
const count = useAtomValue(countAtom)
const setCount = useSetAtom(countAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
Key Features:
- Writable — Can read AND write
- Stable references — Define at module level
- Type-safe — TypeScript infers
Atom.Writable<number>
TMNL Example (src/lib/data-manager/v1/atoms/index.ts:51):
// State atoms (readonly from components)
export const resultsAtom = Atom.make<SearchResult[]>([])
export const statusAtom = Atom.make<StreamStatus>('idle')
export const statsAtom = Atom.make<StreamStats>({ chunks: 0, items: 0, ms: 0 })
Pattern 2: Atom.make with Getter — DERIVED ATOMS
When to use:
- Computed values from other atoms
- Derived state (fullName from firstName + lastName)
- Auto-tracking dependencies
Signature:
Atom.make<A>((get) => A): Atom.Atom<A> // Readonly
Full Example:
import { Atom } from '@effect-atom/atom-react'
const firstNameAtom = Atom.make('John')
const lastNameAtom = Atom.make('Doe')
// Derived atom (auto-tracks dependencies)
const fullNameAtom = Atom.make((get) => {
const first = get(firstNameAtom)
const last = get(lastNameAtom)
return `${first} ${last}`
})
// Another derived atom
const greetingAtom = Atom.make((get) => {
const name = get(fullNameAtom)
return `Hello, ${name}!`
})
// React component
function Greeting() {
const greeting = useAtomValue(greetingAtom)
return <h1>{greeting}</h1> // "Hello, John Doe!"
}
Key Features:
- Readonly — Cannot be written to directly
- Auto-tracks — Re-computes when dependencies change
- Lazy — Only computes when subscribed
TMNL Example (src/lib/data-manager/v1/atoms/index.ts:94):
export const isSearchingAtom = Atom.make((get) => {
const status = get(statusAtom)
return status === 'streaming'
})
export const hasResultsAtom = Atom.make((get) => {
const results = get(resultsAtom)
return results.length > 0
})
Pattern 3: Atom.make with Effect — RESULT ATOMS
When to use:
- Async data (API calls, database queries)
- Fallible operations (can fail with typed errors)
- Need
Initial | Success | Failurestates
Signature:
Atom.make<A, E>(effect: Effect.Effect<A, E>): Atom.Atom<Result<A, E>>
Result Type:
type Result<A, E> =
| { readonly _tag: 'Initial' }
| { readonly _tag: 'Success'; readonly value: A }
| { readonly _tag: 'Failure'; readonly error: E }
Full Example:
import { Atom } from '@effect-atom/atom-react'
import { Effect, Result } from 'effect'
import { useAtomValue } from '@effect-rx/rx-react'
// Effect atom
const userAtom = Atom.make(
Effect.gen(function* () {
yield* Effect.sleep('100 millis')
const response = yield* Effect.tryPromise(() =>
fetch('/api/user').then(r => r.json())
)
return response
})
)
// React component
function UserProfile() {
const result = useAtomValue(userAtom)
// Pattern match on Result
if (Result.isInitial(result)) {
return <div>Loading...</div>
}
if (Result.isFailure(result)) {
return <div>Error: {result.error.message}</div>
}
// Result.isSuccess
return <div>User: {result.value.name}</div>
}
Key Features:
- Three states — Initial, Success, Failure
- Type-safe errors —
Eis typed - Pattern matching —
Result.isSuccess,Result.isFailure,Result.isInitial
TMNL Example (src/components/testbed/EffectAtomTestbed.tsx:1016):
const dataAtom = Atom.make(
Effect.gen(function* () {
yield* Effect.sleep('500 millis')
return { message: 'Hello from Effect!' }
})
)
function EffectExample() {
const result = useAtomValue(dataAtom)
return (
<div>
{Result.isInitial(result) && <Spinner />}
{Result.isSuccess(result) && <div>{result.value.message}</div>}
{Result.isFailure(result) && <Error error={result.error} />}
</div>
)
}
Pattern 4: Atom.runtime — SERVICE COMPOSITION
When to use:
- Need access to Effect services in atoms
- Compose multiple service layers
- Create service-scoped atom runtime
Signature:
Atom.runtime<R, E>(layer: Layer.Layer<R, E>): AtomRuntime<R, E>
Full Example:
import { Atom } from '@effect-atom/atom-react'
import { Effect, Layer } from 'effect'
// Services
class Logger extends Effect.Service<Logger>()(
"app/Logger",
{ effect: Effect.succeed({ log: (msg: string) => Effect.sync(() => console.log(msg)) }) }
) {}
class ApiClient extends Effect.Service<ApiClient>()(
"app/ApiClient",
{
effect: Effect.gen(function* () {
const logger = yield* Logger
return {
fetchUsers: () => Effect.gen(function* () {
yield* logger.log('Fetching users...')
return yield* Effect.tryPromise(() => fetch('/api/users').then(r => r.json()))
})
} as const
}),
dependencies: [Logger.Default]
}
) {}
// Create runtime from composed layers
export const appRuntime = Atom.runtime(
Layer.mergeAll(Logger.Default, ApiClient.Default)
)
// Create atoms with service access
export const usersAtom = appRuntime.atom(
Effect.gen(function* () {
const api = yield* ApiClient
return yield* api.fetchUsers()
})
)
// React component
function UserList() {
const result = useAtomValue(usersAtom)
if (Result.isSuccess(result)) {
return <List items={result.value} />
}
return <Loading />
}
Key Methods on AtomRuntime:
runtime.atom(effect)— Create Result atom with service accessruntime.fn<Arg>()((arg, ctx) => effect)— Create operation atomruntime.pull(stream)— Create pull-based stream atom
TMNL Example (src/lib/data-manager/v1/atoms/index.ts:142):
export const runtimeAtom = Atom.runtime(
Layer.mergeAll(
IdGenerator.Default,
SearchKernel.Default,
DataManager.Default
)
)
Pattern 5: runtimeAtom.fn — OPERATION ATOMS
When to use:
- Mutations (create, update, delete)
- Actions (search, submit, clear)
- Operations that update other atoms
Signature:
runtime.fn<Arg>()((arg, ctx) => Effect.Effect<A, E>): AtomResultFn<Arg, A, E>
Full Example:
import { Atom } from '@effect-atom/atom-react'
import { Effect } from 'effect'
import { useSetAtom, useAtomValue } from '@effect-rx/rx-react'
// State atoms
const resultsAtom = Atom.make<string[]>([])
const statusAtom = Atom.make<'idle' | 'loading' | 'complete'>('idle')
// Operation atom
const searchOp = runtimeAtom.fn<string>()((query, ctx) =>
Effect.gen(function* () {
const api = yield* ApiClient
// Update state
ctx.set(statusAtom, 'loading')
ctx.set(resultsAtom, [])
// Perform search
const results = yield* api.search(query)
// Update state
ctx.set(resultsAtom, results)
ctx.set(statusAtom, 'complete')
return results
})
)
// React component
function SearchBox() {
const doSearch = useSetAtom(searchOp)
const results = useAtomValue(resultsAtom)
const status = useAtomValue(statusAtom)
return (
<div>
<input onChange={(e) => doSearch(e.target.value)} />
{status === 'loading' && <Spinner />}
<List items={results} />
</div>
)
}
Context API (ctx):
ctx.get(atom)— Read atom valuectx.set(atom, value)— Write atom valuectx.setSelf(value)— Write to operation's result atom
Key Features:
- Writable — Trigger with
useSetAtom(op)(arg) - Service access —
yield* ServiceNamein Effect - State updates —
ctx.set(atom, value)directly
TMNL Example (src/lib/data-manager/v1/atoms/index.ts:166):
export const doSearch = runtimeAtom.fn<{ query: string; limit: number }>()(
({ query, limit }, ctx) =>
Effect.gen(function* () {
const dm = yield* DataManager
ctx.set(statusAtom, 'streaming')
ctx.set(resultsAtom, [])
const stream = yield* dm.searchStream(query, limit)
yield* Stream.runForEach(stream, (result) =>
Effect.sync(() => {
const prev = ctx.get(resultsAtom)
ctx.set(resultsAtom, [...prev, result])
})
)
ctx.set(statusAtom, 'complete')
})
)
Pattern 6: Atom.family — KEYED ATOMS
When to use:
- Dynamic atom creation (layer-123, user-456)
- Stable references for same key
- Per-entity atoms
Signature:
Atom.family<K, A>((key: K) => Atom.Atom<A>): (key: K) => Atom.Atom<A>
Full Example:
import { Atom } from '@effect-atom/atom-react'
import { Effect } from 'effect'
// Family of user atoms
const userAtom = Atom.family((id: string) =>
Atom.make(
Effect.gen(function* () {
const api = yield* ApiClient
return yield* api.fetchUser(id)
})
)
)
// Same ID returns same atom instance
const alice1 = userAtom('alice')
const alice2 = userAtom('alice')
console.log(alice1 === alice2) // true
// React component
function UserCard({ userId }: { userId: string }) {
const result = useAtomValue(userAtom(userId))
if (Result.isSuccess(result)) {
return <div>{result.value.name}</div>
}
return <Loading />
}
// Render multiple users
<div>
<UserCard userId="alice" />
<UserCard userId="bob" />
<UserCard userId="charlie" />
</div>
Key Features:
- Stable references — Same key = same atom
- Weak references — Garbage collected when unused
- Type-safe keys — Can use objects, not just strings
TMNL Example (src/components/testbed/EffectAtomTestbed.tsx:1026):
const itemAtom = Atom.family((id: string) =>
Atom.make(
Effect.gen(function* () {
yield* Effect.sleep('200 millis')
return { id, name: `Item ${id}`, timestamp: Date.now() }
})
)
)
Pattern 7: Materialized View Pattern
When to use:
- Separate state (readonly views) from operations (write-only actions)
- Service methods update state via
ctx.set - Clean separation of concerns
Structure:
atoms/
├── state/
│ ├── resultsAtom ← Primitive, readonly from components
│ ├── statusAtom ← Primitive, readonly from components
│ └── statsAtom ← Derived from above
└── operations/
├── searchOp ← runtime.fn, writes to state
└── clearOp ← runtime.fn, writes to state
Full Example:
import { Atom } from '@effect-atom/atom-react'
import { Effect } from 'effect'
// ====== STATE (Readonly Views) ======
export const resultsAtom = Atom.make<SearchResult[]>([])
export const statusAtom = Atom.make<'idle' | 'loading' | 'complete'>('idle')
export const statsAtom = Atom.make((get) => {
const results = get(resultsAtom)
return {
count: results.length,
hasResults: results.length > 0
}
})
// ====== OPERATIONS (Write-Only Actions) ======
export const searchOp = runtimeAtom.fn<string>()((query, ctx) =>
Effect.gen(function* () {
const api = yield* ApiClient
ctx.set(statusAtom, 'loading')
ctx.set(resultsAtom, [])
const results = yield* api.search(query)
ctx.set(resultsAtom, results)
ctx.set(statusAtom, 'complete')
})
)
export const clearOp = runtimeAtom.fn<void>()((_, ctx) =>
Effect.sync(() => {
ctx.set(resultsAtom, [])
ctx.set(statusAtom, 'idle')
})
)
// ====== REACT COMPONENT ======
function SearchUI() {
const results = useAtomValue(resultsAtom)
const status = useAtomValue(statusAtom)
const stats = useAtomValue(statsAtom)
const doSearch = useSetAtom(searchOp)
const clear = useSetAtom(clearOp)
return (
<div>
<input onChange={(e) => doSearch(e.target.value)} />
<button onClick={() => clear()}>Clear</button>
{status === 'loading' && <Spinner />}
<div>Found {stats.count} results</div>
<List items={results} />
</div>
)
}
Key Pattern:
- State atoms — Define at module level, readonly from components
- Operation atoms — Define with
runtime.fn, write to state atoms viactx.set - Components — Subscribe to state, trigger operations
TMNL Example (src/lib/data-manager/v1/atoms/index.ts:34):
// State atoms
export const resultsAtom = Atom.make<SearchResult[]>([])
export const statusAtom = Atom.make<StreamStatus>('idle')
// Operations
export const doSearch = runtimeAtom.fn<{ query: string; limit: number }>()(...)
export const clearResults = runtimeAtom.fn<void>()(...)
Pattern 8: Parallel Atom Setup (Effect.all)
When to use:
- Setting multiple atoms from within an Effect
- Initializing UI state before opening drawer/modal
- Resetting multiple atoms at once
Anti-pattern (sequential):
// DON'T — Sequential updates
yield* Effect.sync(() => registry.set(modeAtom, 'command'))
yield* Effect.sync(() => registry.set(promptAtom, 'M-x '))
yield* Effect.sync(() => registry.set(inputAtom, ''))
yield* Effect.sync(() => registry.set(selectedIndexAtom, 0))
Pattern (parallel):
// DO — Parallel updates with Effect.all
yield* Effect.all([
Effect.sync(() => registry.set(modeAtom, 'command')),
Effect.sync(() => registry.set(promptAtom, 'M-x ')),
Effect.sync(() => registry.set(inputAtom, '')),
Effect.sync(() => registry.set(selectedIndexAtom, 0)),
], { concurrency: 'unbounded' })
Key Features:
- Parallel execution — All updates happen at once
- Effect.sync — Wrap synchronous registry operations
- concurrency: 'unbounded' — No limit on parallelism
TMNL Example (src/lib/minibuffer/hooks/useMinibuffer.tsx:124):
const openCommandMode = runtimeAtom.fn<void>()((_, ctx) =>
Effect.gen(function* () {
yield* Effect.all([
Effect.sync(() => ctx.set(modeAtom, 'command')),
Effect.sync(() => ctx.set(promptAtom, 'M-x ')),
Effect.sync(() => ctx.set(inputAtom, '')),
], { concurrency: 'unbounded' })
})
)
Pattern 9: Atom Lifecycle
keepAlive — Prevent disposal:
const runtimeAtom = Atom.keepAlive(Atom.runtime(ServiceLayer))
setIdleTTL — Dispose after idle:
const cachedAtom = Atom.setIdleTTL(Atom.make(expensiveFetch), '5 minutes')
autoDispose — Default behavior (dispose when no subscribers).
Examples
Example 1: Counter (Primitive Atom)
import { Atom } from '@effect-atom/atom-react'
import { useAtomValue, useSetAtom } from '@effect-rx/rx-react'
const countAtom = Atom.make(0)
function Counter() {
const count = useAtomValue(countAtom)
const setCount = useSetAtom(countAtom)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
Example 2: Todo List (Operation Atoms)
import { Atom } from '@effect-atom/atom-react'
import { Effect } from 'effect'
type Todo = { id: string; text: string; done: boolean }
const todosAtom = Atom.make<Todo[]>([])
const addTodoOp = runtimeAtom.fn<string>()((text, ctx) =>
Effect.sync(() => {
const todos = ctx.get(todosAtom)
ctx.set(todosAtom, [...todos, { id: nanoid(), text, done: false }])
})
)
const toggleTodoOp = runtimeAtom.fn<string>()((id, ctx) =>
Effect.sync(() => {
const todos = ctx.get(todosAtom)
ctx.set(todosAtom, todos.map(t => t.id === id ? { ...t, done: !t.done } : t))
})
)
function TodoList() {
const todos = useAtomValue(todosAtom)
const addTodo = useSetAtom(addTodoOp)
const toggleTodo = useSetAtom(toggleTodoOp)
return (
<div>
<input onKeyDown={(e) => {
if (e.key === 'Enter') addTodo(e.currentTarget.value)
}} />
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.done ? '✅' : '⬜'} {todo.text}
</li>
))}
</ul>
</div>
)
}
Anti-Patterns
1. Effect.Ref in React-consumed Services (BANNED)
// WRONG — Ref + Atom bridging complexity
const service = Effect.gen(function* () {
const stateRef = yield* Ref.make(initial)
// Now need: polling, SubscriptionRef, streams-to-consume-streams
})
// CORRECT — Atom-as-State
const stateAtom = Atom.make(initial)
const service = {
update: (value) => Effect.sync(() => Atom.set(stateAtom, value))
}
2. Atoms Inside Components
// WRONG — Creates new atom every render
function MyComponent() {
const atom = Atom.make(0) // ❌ New atom every render!
return <div>{useAtomValue(atom)}</div>
}
// CORRECT — Module-level atom
const countAtom = Atom.make(0)
function MyComponent() {
return <div>{useAtomValue(countAtom)}</div>
}
3. useState for Cross-Component State
// WRONG — useState for shared state
const [results, setResults] = useState([])
const [status, setStatus] = useState('idle')
// CORRECT — Atoms
const resultsAtom = Atom.make([])
const statusAtom = Atom.make('idle')
Quick Reference
| Pattern | Constructor | Use Case |
|---|---|---|
| Primitive atom | Atom.make(value) | Simple mutable state |
| Derived atom | Atom.make((get) => ...) | Computed from other atoms |
| Effect atom | Atom.make(Effect) | Async data with Result |
| Runtime | Atom.runtime(Layer) | Service composition |
| Operation | runtime.fn<Arg>() | Mutations/actions |
| Family | Atom.family((key) => ...) | Keyed atoms |
Related Skills
- effect-service-authoring — Create services for use with runtimeAtom
- effect-stream-patterns — Use streams with Atom.make(Stream)
- effect-testing-patterns — Test atom-based code