atom-state
from front-depiction/claude-setup
Reusable Claude Code configuration for Effect TypeScript projects with specialized agents and skills
npx skills add https://github.com/front-depiction/claude-setup --skill atom-stateSKILL.md
Effect Atom State Management
Effect Atom is a reactive state management library for Effect that seamlessly integrates with React.
Core Concepts
Atoms as References
Atoms work by reference - they are stable containers for reactive state:
import * as Atom from "@effect-atom/atom-react"
// Atoms are created once and referenced throughout the app
export const counterAtom = Atom.make(0)
// Multiple components can reference the same atom
// All update when the atom value changes
Automatic Cleanup
Atoms automatically reset when no subscribers remain (unless marked with keepAlive):
// Resets when last subscriber unmounts
export const temporaryState = Atom.make(initialValue)
// Persists across component lifecycles
export const persistentState = Atom.make(initialValue).pipe(Atom.keepAlive)
Lazy Evaluation
Atom values are computed on-demand when subscribers access them.
Pattern: Basic Atoms
import * as Atom from "@effect-atom/atom-react"
// Simple atom
export const count = Atom.make(0)
// Atom with object state
export interface CartState {
readonly items: ReadonlyArray<Item>
readonly total: number
}
export const cart = Atom.make<CartState>({
items: [],
total: 0
})
Pattern: Derived Atoms
Use Atom.map or computed atoms with the get parameter:
// Derived via map
export const itemCount = Atom.map(cart, (c) => c.items.length)
export const isEmpty = Atom.map(cart, (c) => c.items.length === 0)
// Computed atom accessing other atoms
export const cartSummary = Atom.make((get) => {
const cartData = get(cart)
const count = get(itemCount)
return {
itemCount: count,
total: cartData.total,
isEmpty: count === 0
}
})
Pattern: Atom Family (Dynamic Atoms)
Use Atom.family for stable references to dynamically created atoms:
// Create atoms per entity ID
export const userAtoms = Atom.family((userId: string) =>
Atom.make<User | null>(null).pipe(Atom.keepAlive)
)
// Usage - always returns the same atom for a given ID
const userAtom = userAtoms(userId)
Pattern: Atom.fn for Async Actions
Use Atom.fn with Effect.fnUntraced for async operations:
- Reading gives
Result<Success, Error>with automatic.waitingflag - Triggering via
useAtomSetruns the effect
import { Atom, useAtomValue, useAtomSet } from "@effect-atom/atom-react"
import { Effect, Exit } from "effect"
// Atom.fn with Effect.fnUntraced for generator syntax
const logAtom = Atom.fn(
Effect.fnUntraced(function* (arg: number) {
yield* Effect.log("got arg", arg)
})
)
function LogComponent() {
// useAtomSet returns a trigger function
const logNumber = useAtomSet(logAtom)
return <button onClick={() => logNumber(42)}>Log 42</button>
}
With services using Atom.runtime:
class Users extends Effect.Service<Users>()("app/Users", {
effect: Effect.gen(function* () {
const create = (name: string) => Effect.succeed({ id: 1, name })
return { create } as const
}),
}) {}
const runtimeAtom = Atom.runtime(Users.Default)
// runtimeAtom.fn provides service access
const createUserAtom = runtimeAtom.fn(
Effect.fnUntraced(function* (name: string) {
const users = yield* Users
return yield* users.create(name)
})
)
function CreateUserComponent() {
// mode: "promiseExit" for async handlers with Exit result
const createUser = useAtomSet(createUserAtom, { mode: "promiseExit" })
return (
<button onClick={async () => {
const exit = await createUser("John")
if (Exit.isSuccess(exit)) {
console.log(exit.value)
}
}}>
Create user
</button>
)
}
Reading result state:
function UserList() {
const [result, createUser] = useAtom(createUserAtom) // Result<User, Error>
// Use matchWithWaiting for proper waiting state handling
return Result.matchWithWaiting(result, {
onWaiting: () => <Spinner />,
onSuccess: ({ value }) => <UserCard user={value} />,
onError: (error) => <Error message={String(error)} />,
onDefect: (defect) => <Error message={String(defect)} />
})
}
Anti-pattern: Manual void wrappers
// ❌ DON'T - manual state management loses waiting control
const loading$ = Atom.make(false)
const user$ = Atom.make<User | null>(null)
const fetchUser = (id: string): void => {
registry.set(loading$, true)
Effect.runPromise(userService.getById(id)).then(user => {
registry.set(user$, user)
registry.set(loading$, false)
})
}
// ✅ DO - Atom.fn handles loading/success/failure automatically
const fetchUserAtom = Atom.fn(
Effect.fnUntraced(function* (id: string) {
return yield* userService.getById(id)
})
)
// result.waiting, Result.match - all built-in
Pattern: Runtime with Services
Wrap Effect layers/services for use in atoms:
import { Layer } from "effect"
// Create runtime with services
expo
...