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:
typescript
1import * as Atom from "@effect-atom/atom-react"
2
3// Atoms are created once and referenced throughout the app
4export const counterAtom = Atom.make(0)
5
6// Multiple components can reference the same atom
7// All update when the atom value changes
Automatic Cleanup
Atoms automatically reset when no subscribers remain (unless marked with keepAlive):
typescript
1// Resets when last subscriber unmounts
2export const temporaryState = Atom.make(initialValue)
3
4// Persists across component lifecycles
5export const persistentState = Atom.make(initialValue).pipe(Atom.keepAlive)
Lazy Evaluation
Atom values are computed on-demand when subscribers access them.
Pattern: Basic Atoms
typescript
1import * as Atom from "@effect-atom/atom-react"
2
3// Simple atom
4export const count = Atom.make(0)
5
6// Atom with object state
7export interface CartState {
8 readonly items: ReadonlyArray<Item>
9 readonly total: number
10}
11
12export const cart = Atom.make<CartState>({
13 items: [],
14 total: 0
15})
Pattern: Derived Atoms
Use Atom.map or computed atoms with the get parameter:
typescript
1// Derived via map
2export const itemCount = Atom.map(cart, (c) => c.items.length)
3export const isEmpty = Atom.map(cart, (c) => c.items.length === 0)
4
5// Computed atom accessing other atoms
6export const cartSummary = Atom.make((get) => {
7 const cartData = get(cart)
8 const count = get(itemCount)
9
10 return {
11 itemCount: count,
12 total: cartData.total,
13 isEmpty: count === 0
14 }
15})
Pattern: Atom Family (Dynamic Atoms)
Use Atom.family for stable references to dynamically created atoms:
typescript
1// Create atoms per entity ID
2export const userAtoms = Atom.family((userId: string) =>
3 Atom.make<User | null>(null).pipe(Atom.keepAlive)
4)
5
6// Usage - always returns the same atom for a given ID
7const userAtom = userAtoms(userId)
Pattern: Function Atoms (Side Effects)
Use Atom.fn for operations with side effects:
typescript
1import { Effect } from "effect"
2
3// Function atom for side effects
4export const addItem = Atom.fn(
5 Effect.fnUntraced(function* (item: Item) {
6 const current = yield* Atom.get(cart)
7
8 yield* Atom.set(cart, {
9 items: [...current.items, item],
10 total: current.total + item.price
11 })
12 })
13)
14
15// Clear cart operation
16export const clearCart = Atom.fn(
17 Effect.fnUntraced(function* () {
18 yield* Atom.set(cart, { items: [], total: 0 })
19 })
20)
Pattern: Runtime with Services
Wrap Effect layers/services for use in atoms:
typescript
1import { Layer } from "effect"
2
3// Create runtime with services
4export const runtime = Atom.runtime(
5 Layer.mergeAll(
6 DatabaseService.Live,
7 LoggerService.Live,
8 ApiClient.Live
9 )
10)
11
12// Use services in function atoms
13export const fetchUserData = runtime.fn(
14 Effect.fnUntraced(function* (userId: string) {
15 const db = yield* DatabaseService
16 const user = yield* db.getUser(userId)
17
18 yield* Atom.set(userAtoms(userId), user)
19 return user
20 })
21)
Global Layers
Configure global layers once at app initialization:
typescript
1// App setup
2Atom.runtime.addGlobalLayer(
3 Layer.mergeAll(
4 Logger.Live,
5 Tracer.Live,
6 Config.Live
7 )
8)
Pattern: Result Types (Error Handling)
Atoms can return Result types for explicit error handling:
typescript
1import * as Result from "@effect-atom/atom/Result"
2
3export const userData = Atom.make<Result.Result<User, Error>>(
4 Result.initial
5)
6
7// In component
8const result = useAtomValue(userData)
9
10Result.match(result, {
11 Initial: () => <Loading />,
12 Failure: (error) => <Error message={error.message} />,
13 Success: (user) => <UserProfile user={user} />
14})
Pattern: Stream Integration
Convert streams into atoms that capture the latest value:
typescript
1import { Stream } from "effect"
2
3// Infinite stream becomes reactive atom
4export const notifications = Atom.make(
5 Stream.fromEventListener(window, "notification").pipe(
6 Stream.map(parseNotification),
7 Stream.filter(isValid),
8 Stream.scan([], (acc, n) => [...acc, n].slice(-10))
9 )
10)
Use Atom.pull for stream-based pagination:
typescript
1export const pagedItems = Atom.pull(
2 Stream.fromIterable(itemsSource).pipe(
3 Stream.grouped(10) // Pages of 10 items
4 )
5)
6
7// In component - automatically fetches next page when called
8const loadMore = useAtomSet(pagedItems)
Pattern: Persistence
Use Atom.kvs for persisted state:
typescript
1import { BrowserKeyValueStore } from "@effect/platform-browser"
2import * as Schema from "effect/Schema"
3
4export const userSettings = Atom.kvs({
5 runtime: Atom.runtime(BrowserKeyValueStore.layerLocalStorage),
6 key: "user-settings",
7 schema: Schema.Struct({
8 theme: Schema.Literal("light", "dark"),
9 notifications: Schema.Boolean,
10 language: Schema.String
11 }),
12 defaultValue: () => ({
13 theme: "light",
14 notifications: true,
15 language: "en"
16 })
17})
React Integration
Hooks
typescript
1import { useAtomValue, useAtomSet, useAtom, useAtomSetPromise } from "@effect-atom/atom-react"
2
3export function CartView() {
4 // Read only
5 const cartData = useAtomValue(cart)
6 const isEmpty = useAtomValue(isEmpty)
7
8 // Write only
9 const addItem = useAtomSet(addItem)
10 const clearCart = useAtomSet(clearCart)
11
12 // Both read and write
13 const [count, setCount] = useAtom(counterAtom)
14
15 // For async function atoms
16 const fetchData = useAtomSetPromise(fetchUserData)
17
18 return (
19 <div>
20 <div>Items: {cartData.items.length}</div>
21 <button onClick={() => addItem(newItem)}>Add</button>
22 <button onClick={() => clearCart()}>Clear</button>
23 </div>
24 )
25}
Separation of Concerns
Different components can read/write the same atom reactively:
typescript
1// Component A - reads state
2function CartDisplay() {
3 const cart = useAtomValue(cart)
4 return <div>Items: {cart.items.length}</div>
5}
6
7// Component B - modifies state
8function CartActions() {
9 const addItem = useAtomSet(addItem)
10 return <button onClick={() => addItem(item)}>Add</button>
11}
12
13// Both update reactively when atom changes
Scoped Resources & Finalizers
Atoms support scoped effects with automatic cleanup:
typescript
1export const wsConnection = Atom.make(
2 Effect.gen(function* () {
3 // Acquire resource
4 const ws = yield* Effect.acquireRelease(
5 connectWebSocket(),
6 (ws) => Effect.sync(() => ws.close())
7 )
8
9 return ws
10 })
11)
12
13// Finalizer runs when atom rebuilds or becomes unused
Key Principles
- Reference Stability: Use
Atom.family for dynamically generated atom sets
- Lazy Evaluation: Values computed on-demand when accessed
- Automatic Cleanup: Atoms reset when unused (unless
keepAlive)
- Derive, Don't Coordinate: Use computed atoms to derive state
- Result Types: Handle errors explicitly with Result.match
- Services in Runtime: Wrap layers once, use in multiple atoms
- Immutable Updates: Always create new values, never mutate
- Scoped Effects: Leverage finalizers for resource cleanup
Common Patterns
Loading States
typescript
1export const userDataAtom = Atom.make<Result.Result<User, Error>>(
2 Result.initial
3)
4
5export const loadUser = runtime.fn(
6 Effect.fnUntraced(function* (id: string) {
7 yield* Atom.set(userDataAtom, Result.initial)
8
9 const result = yield* Effect.either(
10 userService.fetchUser(id)
11 )
12
13 yield* Atom.set(
14 userDataAtom,
15 result._tag === "Right"
16 ? Result.success(result.right)
17 : Result.failure(result.left)
18 )
19 })
20)
Optimistic Updates
typescript
1export const updateItem = runtime.fn(
2 Effect.fnUntraced(function* (id: string, updates: Partial<Item>) {
3 const current = yield* Atom.get(itemsAtom)
4
5 // Optimistic update
6 yield* Atom.set(
7 itemsAtom,
8 current.map(item => item.id === id ? { ...item, ...updates } : item)
9 )
10
11 // Persist to server
12 const result = yield* Effect.either(api.updateItem(id, updates))
13
14 // Revert on failure
15 if (result._tag === "Left") {
16 yield* Atom.set(itemsAtom, current)
17 }
18 })
19)
Computed Queries
typescript
1// Filter atom accessing other atoms
2export const filteredItems = Atom.make((get) => {
3 const items = get(itemsAtom)
4 const searchTerm = get(searchAtom)
5 const activeFilters = get(filtersAtom)
6
7 return items.filter(item =>
8 item.name.includes(searchTerm) &&
9 activeFilters.every(f => f.predicate(item))
10 )
11})
Effect Atom bridges Effect's powerful type system with React's rendering model, providing type-safe reactive state management with automatic cleanup and seamless Effect integration.