Claude-skill-registry effect-atom-integration
Atom.runtime, Atom.make, runtimeAtom.fn, operation atoms, Result handling. Atom-as-State doctrine for integrating Effect services with React.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/effect-atom-integration" ~/.claude/skills/majiayu000-claude-skill-registry-effect-atom-integration && rm -rf "$T"
skills/data/effect-atom-integration/SKILL.mdEffect-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,
is the primary state mechanism—notAtom.make()inside services.Effect.Ref
- 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.make (Effect detection)Atom.ts:370-391
— Writable atomsAtom.ts:458-463
— Readable (derived) atomsAtom.ts:328-338
— Atom.runtimeAtom.ts:643-715
— AtomRuntime.fn (operation atoms)Atom.ts:553-588
— Atom.familyAtom.ts:1316-1351
effect-atom Tests
- Submodule:
../../submodules/effect-atom/packages/atom/test/
— Canonical usage patternsAtom.test.ts
— Result type handlingResult.test.ts
TMNL Battle-tested Implementations
- DataManager atoms —
(canonical Atom-as-State)src/lib/data-manager/v1/atoms/index.ts - Slider atoms —
(runtime factories)src/lib/slider/v1/atoms/index.ts - Testbed —
(comprehensive examples)src/components/testbed/EffectAtomTestbed.tsx
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
statesInitial | Success | Failure
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 —
is typedE - Pattern matching —
,Result.isSuccess
,Result.isFailureResult.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:
— Create Result atom with service accessruntime.atom(effect)
— Create operation atomruntime.fn<Arg>()((arg, ctx) => effect)
— Create pull-based stream atomruntime.pull(stream)
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
— Read atom valuectx.get(atom)
— Write atom valuectx.set(atom, value)
— Write to operation's result atomctx.setSelf(value)
Key Features:
- Writable — Trigger with
useSetAtom(op)(arg) - Service access —
in Effectyield* ServiceName - State updates —
directlyctx.set(atom, value)
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
, write to state atoms viaruntime.fnctx.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 | | Simple mutable state |
| Derived atom | | Computed from other atoms |
| Effect atom | | Async data with Result |
| Runtime | | Service composition |
| Operation | | Mutations/actions |
| Family | | 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