| name | effect-atom-state |
| description | Implement reactive state management with Effect Atom for React applications |
Effect Atom State Management
Effect Atom is a reactive state management library for Effect that seamlessly integrates with React.
Effect Source Reference
The Effect v4 source is available at ~/.cache/effect-v4/.
Browse and read files there directly to look up APIs, types, and implementations.
Reference this for:
- Atom reactivity:
packages/effect/src/unstable/reactivity/
- AsyncResult source:
packages/effect/src/unstable/reactivity/AsyncResult.ts
- Effect source:
packages/effect/src/
Core Concepts
Atoms as References
Atoms work by reference - they are stable containers for reactive state:
import * as Atom from 'effect/unstable/reactivity/Atom';
export const counterAtom = Atom.make(0);
Automatic Cleanup
Atoms automatically reset when no subscribers remain (unless marked with keepAlive):
export const temporaryState = Atom.make(initialValue);
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/unstable/reactivity/Atom';
export const count = Atom.make(0);
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:
export const itemCount = Atom.map(cart, (c) => c.items.length);
export const isEmpty = Atom.map(cart, (c) => c.items.length === 0);
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:
export const userAtoms = Atom.family((userId: string) =>
Atom.make<User | null>(null).pipe(Atom.keepAlive)
);
const userAtom = userAtoms(userId);
Pattern: Atom.fn for Async Actions
Use Atom.fn with Effect.fnUntraced for async operations:
- Reading gives
AsyncResult<Success, Error> with automatic .waiting flag
- Triggering via
useAtomSet runs the effect
import * as Atom from "effect/unstable/reactivity/Atom"
import { useAtomValue, useAtomSet } from "@effect/atom-react"
import { Effect, Exit } from "effect"
const logAtom = Atom.fn(
Effect.fnUntraced(function* (arg: number) {
yield* Effect.log("got arg", arg)
})
)
function LogComponent() {
const logNumber = useAtomSet(logAtom)
return <button onClick={() => logNumber(42)}>Log 42</button>
}
With services using Atom.runtime:
class Users extends Context.Service<Users>()("app/Users", {
make: Effect.gen(function* () {
const create = (name: string) => Effect.succeed({ id: 1, name })
return { create } as const
}),
}) {}
const runtimeAtom = Atom.runtime(Users.layer)
const createUserAtom = runtimeAtom.fn(
Effect.fnUntraced(function* (name: string) {
const users = yield* Users
return yield* users.create(name)
})
)
function CreateUserComponent() {
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)
return AsyncResult.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
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);
});
};
const fetchUserAtom = Atom.fn(
Effect.fnUntraced(function* (id: string) {
return yield* userService.getById(id);
})
);
Pattern: Runtime with Services
Wrap Effect layers/services for use in atoms:
import { Layer } from 'effect';
export const runtime = Atom.runtime(
Layer.mergeAll(DatabaseService.Live, LoggerService.Live, ApiClient.Live)
);
export const fetchUserData = runtime.fn(
Effect.fnUntraced(function* (userId: string) {
const db = yield* DatabaseService;
const user = yield* db.getUser(userId);
yield* Atom.set(userAtoms(userId), user);
return user;
})
);
Global Layers
Configure global layers once at app initialization:
Atom.runtime.addGlobalLayer(
Layer.mergeAll(Logger.Live, Tracer.Live, Config.Live)
);
Pattern: AsyncResult Types (Error Handling)
Atoms can return AsyncResult types for explicit error handling:
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
export const userData = Atom.make<AsyncResult.AsyncResult<User, Error>>(
AsyncResult.initial()
);
const result = useAtomValue(userData);
AsyncResult.matchWithWaiting(result, {
onWaiting: () => <Loading />,
onSuccess: ({ value }) => <UserProfile user={value} />,
onError: (error) => <Error message={String(error)} />,
onDefect: (defect) => <Error message={String(defect)} />
});
Pattern: Stream Integration
Convert streams into atoms that capture the latest value:
import { Stream } from 'effect';
export const notifications = Atom.make(
Stream.fromEventListener(window, 'notification').pipe(
Stream.map(parseNotification),
Stream.filter(isValid),
Stream.scan([], (acc, n) => [...acc, n].slice(-10))
)
);
Pattern: Pull Atoms (Pagination)
Use Atom.pull for stream-based pagination:
export const pagedItems = Atom.pull(
Stream.fromIterable(itemsSource).pipe(
Stream.grouped(10)
)
);
function PagedList() {
const result = useAtomValue(pagedItems);
const pullNext = useAtomSet(pagedItems);
return AsyncResult.matchWithWaiting(result, {
onWaiting: () => <Loading />,
onError: (error) => <Error message={String(error)} />,
onDefect: (defect) => <Error message={String(defect)} />,
onSuccess: (success) => (
<>
<ItemList items={success.value.items} />
<button
disabled={success.value.done}
onClick={() => pullNext()}
>
Load more
</button>
</>
)
});
}
Atom.pull returns a writable PullResult, which is an AsyncResult whose success value is { done, items }. The waiting flag stays on the top-level result; read pages from success.value.items and call useAtomSet(pagedItems)() to pull the next chunk.
Pattern: Persistence
Use Atom.kvs for persisted state:
import { BrowserKeyValueStore as BrowserKvs } from '@effect/platform-browser';
import * as Atom from 'effect/unstable/reactivity/Atom';
import * as Schema from 'effect/Schema';
export const userSettings = Atom.kvs({
runtime: Atom.runtime(BrowserKvs.layerLocalStorage),
key: 'user-settings',
schema: Schema.Struct({
theme: Schema.Literals(['light', 'dark']),
notifications: Schema.Boolean,
language: Schema.String
}),
defaultValue: () => ({
theme: 'light',
notifications: true,
language: 'en'
})
});
If you want to use the core Web Storage layer directly, import the module namespace and pass Atom.runtime(KeyValueStore.layerStorage(() => globalThis.localStorage)).
React Integration
Hooks
import { useAtomValue, useAtomSet, useAtom } from '@effect/atom-react';
export function CartView() {
const cartData = useAtomValue(cart);
const isEmpty = useAtomValue(isEmpty);
const addItem = useAtomSet(addItem);
const clearCart = useAtomSet(clearCart);
const [count, setCount] = useAtom(counterAtom);
const fetchData = useAtomSet(fetchUserData, { mode: 'promiseExit' });
return (
<div>
<div>Items: {cartData.items.length}</div>
<button onClick={() => addItem(newItem)}>Add</button>
<button onClick={() => clearCart()}>Clear</button>
</div>
);
}
Separation of Concerns
Different components can read/write the same atom reactively:
function CartDisplay() {
const cart = useAtomValue(cart);
return <div>Items: {cart.items.length}</div>;
}
function CartActions() {
const addItem = useAtomSet(addItem);
return <button onClick={() => addItem(item)}>Add</button>;
}
Scoped Resources & Finalizers
Atoms support scoped effects with automatic cleanup:
export const wsConnection = Atom.make(
Effect.gen(function* () {
const ws = yield* Effect.acquireRelease(connectWebSocket(), (ws) =>
Effect.sync(() => ws.close())
);
return ws;
})
);
Key Principles
- Atom.fn for Async: Use
Atom.fn() for effects—gives automatic waiting flag and AsyncResult type
- Never Manual Void Wrappers: Don't wrap Effects in void functions—you lose
waiting control
- 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 AsyncResult.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
Use Atom.fn with Effect.fnUntraced which automatically provides AsyncResult with .waiting flag:
import * as Atom from "effect/unstable/reactivity/Atom"
import { useAtomValue, useAtomSet } from "@effect/atom-react"
import { Effect } from "effect"
const loadUserAtom = Atom.fn(
Effect.fnUntraced(function* (id: string) {
return yield* userService.fetchUser(id)
})
)
function UserProfile() {
const [result, loadUser] = useAtom(loadUserAtom)
return AsyncResult.matchWithWaiting(result, {
onWaiting: () => <Loading />,
onSuccess: ({ value }) => <UserCard user={value} />,
onError: (error) => <Error message={String(error)} />,
onDefect: (defect) => <Error message={String(defect)} />
})
}
Optimistic Updates
export const updateItem = runtime.fn(
Effect.fnUntraced(function* (id: string, updates: Partial<Item>) {
const current = yield* Atom.get(itemsAtom);
yield* Atom.set(
itemsAtom,
current.map((item) =>
item.id === id ? { ...item, ...updates } : item
)
);
const result = yield* Effect.result(api.updateItem(id, updates));
if (result._tag === 'Failure') {
yield* Atom.set(itemsAtom, current);
}
})
);
Computed Queries
export const filteredItems = Atom.make((get) => {
const items = get(itemsAtom);
const searchTerm = get(searchAtom);
const activeFilters = get(filtersAtom);
return items.filter(
(item) =>
item.name.includes(searchTerm) &&
activeFilters.every((f) => f.predicate(item))
);
});
External push sources
For browser APIs or other external sources that push updates, create an atom with Atom.make((get) => ...) and use get.setSelf plus get.addFinalizer:
export const systemThemeAtom = Atom.make((get) => {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const readTheme = (): 'light' | 'dark' =>
mql.matches ? 'dark' : 'light';
const handler = (event: MediaQueryListEvent) => {
get.setSelf(event.matches ? 'dark' : 'light');
};
mql.addEventListener('change', handler);
get.addFinalizer(() => mql.removeEventListener('change', handler));
return readTheme();
});
Use Atom.transform only to transform an existing source atom; it is not a standalone constructor that accepts an initial value and subscription effect.
Atom.batch
Batch multiple atom updates into a single notification cycle:
Atom.batch(() => {
registry.set(nameAtom, 'Alice');
registry.set(ageAtom, 30);
registry.set(statusAtom, 'active');
});
Outside Effect/Atom contexts, use an AtomRegistry (registry.set(...)) inside the batch. Inside an atom or write context, use that context (ctx.set(...)) in the same pattern. Atom.batch only batches notifications; it does not introduce a free set function.
Use when multiple atoms must update atomically to avoid intermediate renders.
AsyncResult.builder
Chainable API for handling AsyncResult types — replaces verbose AsyncResult.match/AsyncResult.matchWithWaiting:
const UserProfile = ({ userId }: { userId: string }) => {
const user = useAtomValue(userAtom(userId))
return AsyncResult.builder(user)
.onInitial(() => <LoadingSkeleton />)
.onErrorTag('UserNotFound', (err) => <NotFound id={err.userId} />)
.onErrorIf(
(err): err is NetworkError => err instanceof NetworkError,
() => <RetryPrompt />
)
.onError((err) => <Error message={String(err)} />)
.onSuccess((user) => <ProfileCard user={user} />)
.render()
}
onInitial — initial/pending state
onError — handle any typed error value
onErrorIf — handle typed errors with a predicate or refinement
onErrorTag — handle tagged errors by _tag
onFailure — receives the whole Cause.Cause<E>; reserve it for cause-level fallback handling
onSuccess — render the success value
render() — finalize and return JSX; it throws unhandled failures, so handle every expected error/defect or use orElse / orNull
useAtomMount
Activate a side-effect atom without reading its value:
useAtomMount(websocketAtom);
useAtomMount(pollingAtom);
Use when an atom's side effects matter but its value doesn't need to be rendered.
Performance
Selective Re-rendering
Derive focused atoms to avoid unnecessary re-renders:
const user = useAtomValue(userAtom)
return <span>{user.name}</span>
const userName = useMemo(() => Atom.map(userAtom, (u) => u.name), [])
const name = useAtomValue(userName)
return <span>{name}</span>
Use Atom.map to create narrow slices of state that minimize re-render surface.
Anti-Patterns
atoms inside components → creates new atom every render; define outside or useMemo
missing finalizers → memory/subscription leaks; always clean up in transform/fn
missing keepAlive → global state garbage collected; use Atom.keepAlive
ignoring AsyncResult types → crashes on error states; always handle all AsyncResult variants
updating state during render → infinite loops; use effects or event handlers
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.