Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tim-smart/effect-atom/llms.txt
Use this file to discover all available pages before exploring further.
Function atoms let you create reactive functions that execute Effect computations. Unlike regular atoms that represent state, function atoms represent operations that can be triggered on demand.
Basic function atoms
Create a function atom using Atom.fn:
import { Atom, useAtomSet } from "@effect-atom/atom-react"
import { Effect } from "effect"
// Create a simple `Atom.fn` that logs a number
const logAtom = Atom.fn(
Effect.fnUntraced(function* (arg: number) {
yield* Effect.log("got arg", arg)
}),
)
function LogComponent() {
// To call the `Atom.fn`, we need to use the `useAtomSet` hook
const logNumber = useAtomSet(logAtom)
return <button onClick={() => logNumber(42)}>Log 42</button>
}
Function signatures
The Atom.fn function has several overloads for different use cases:
// Type-first style (specify argument type upfront)
const fn1 = Atom.fn<string>()(Effect.fnUntraced(function* (arg) {
// arg is inferred as string
yield* Effect.log(arg)
}))
// Direct style (argument type inferred)
const fn2 = Atom.fn(
Effect.fnUntraced(function* (arg: number) {
yield* Effect.log(arg)
})
)
// With void argument
const fn3 = Atom.fn(
Effect.fnUntraced(function* () {
yield* Effect.log("no args")
})
)
Using with services
Function atoms can access Effect services when created from an AtomRuntime:
import { Atom } from "@effect-atom/atom-react"
import { Effect } from "effect"
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)
// Here we are using `runtimeAtom.fn` to create a function from the `Users.create`
// method.
const createUserAtom = runtimeAtom.fn(
Effect.fnUntraced(function* (name: string) {
const users = yield* Users
return yield* users.create(name)
}),
)
function CreateUserComponent() {
// If your function returns a `Result`, you can use the useAtomSet hook with `mode: "promiseExit"`
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>
)
}
Function atom interface
Function atoms implement the AtomResultFn interface:
interface AtomResultFn<Arg, A, E = never> extends Writable<Result.Result<A, E>, Arg | Reset | Interrupt> {}
This means a function atom is actually a writable atom that:
- Reads as a
Result of the last execution
- Writes by accepting an argument to trigger a new execution
Reading function atom state
You can read the state of a function atom to see the result of the last execution:
import { Atom, Result, useAtom, useAtomValue } from "@effect-atom/atom-react"
import { Effect } from "effect"
const multiplyAtom = Atom.fn(
Effect.fnUntraced(function* (n: number) {
yield* Effect.sleep("1 second")
return n * 2
}),
)
function MultiplyComponent() {
const [result, multiply] = useAtom(multiplyAtom)
return (
<div>
<button onClick={() => multiply(5)}>Multiply 5</button>
<button onClick={() => multiply(10)}>Multiply 10</button>
{Result.builder(result)
.onInitial(() => <p>Click a button to start</p>)
.onSuccess((value, { waiting }) => (
<p>
Result: {value}
{waiting && " (calculating...)"}
</p>
))
.onFailure((cause) => <p>Error: {Cause.pretty(cause)}</p>)
.render()
}
</div>
)
}
Initial values
Provide an initial value to display before the first execution:
const multiplyAtom = Atom.fn(
Effect.fnUntraced(function* (n: number) {
return n * 2
}),
{ initialValue: 0 }
)
// The Result will be Success(0) before any execution
Concurrent execution
By default, a new execution cancels the previous one. Enable concurrent execution to allow multiple executions to run in parallel:
const fetchAtom = Atom.fn(
Effect.fnUntraced(function* (id: string) {
yield* Effect.sleep("2 seconds")
return yield* fetchData(id)
}),
{ concurrent: true }
)
// Multiple calls can now run simultaneously
fetchAtom.write(ctx, "id1") // Starts execution 1
fetchAtom.write(ctx, "id2") // Starts execution 2 (doesn't cancel execution 1)
When using concurrent mode, the Result will reflect the state of all running executions. The atom will show waiting: true until all executions complete.
Resetting function atoms
Use the Reset symbol to clear the function atom state back to initial:
import { Atom } from "@effect-atom/atom-react"
const myFn = Atom.fn(Effect.fnUntraced(function* (n: number) {
return n * 2
}))
function Component() {
const setFn = useAtomSet(myFn)
return (
<div>
<button onClick={() => setFn(10)}>Execute</button>
<button onClick={() => setFn(Atom.Reset)}>Reset</button>
</div>
)
}
Interrupting execution
Use the Interrupt symbol to cancel the current execution:
import { Atom } from "@effect-atom/atom-react"
const longRunningFn = Atom.fn(
Effect.fnUntraced(function* () {
yield* Effect.sleep("10 seconds")
return "done"
})
)
function Component() {
const setFn = useAtomSet(longRunningFn)
return (
<div>
<button onClick={() => setFn()}>Start</button>
<button onClick={() => setFn(Atom.Interrupt)}>Cancel</button>
</div>
)
}
Function context
Function atoms receive a FnContext which provides access to other atoms:
import { Atom } from "@effect-atom/atom-react"
import { Effect } from "effect"
const multiplierAtom = Atom.make(2)
const multiplyAtom = Atom.fn(
Effect.fnUntraced(function* (n: number, get: Atom.FnContext) {
const multiplier = get(multiplierAtom)
return n * multiplier
})
)
The FnContext interface provides:
interface FnContext {
// Read an atom
<A>(atom: Atom<A>): A
// Read a Result atom as Effect
result<A, E>(atom: Atom<Result.Result<A, E>>): Effect.Effect<A, E>
// Add cleanup function
addFinalizer(f: () => void): void
// Mount an atom (keep it alive)
mount<A>(atom: Atom<A>): void
// Refresh an atom
refresh<A>(atom: Atom<A>): void
// Access/set own state
self<A>(): Option.Option<A>
setSelf<A>(a: A): void
// Set other atoms
set<R, W>(atom: Writable<R, W>, value: W): void
setResult<A, E, W>(atom: Writable<Result.Result<A, E>, W>, value: W): Effect.Effect<A, E>
// Convert atom to stream
stream<A>(atom: Atom<A>, options?: { ... }): Stream.Stream<A>
streamResult<A, E>(atom: Atom<Result.Result<A, E>>, options?: { ... }): Stream.Stream<A, E>
// Subscribe to changes
subscribe<A>(atom: Atom<A>, f: (a: A) => void, options?: { ... }): void
// Access registry
readonly registry: Registry.Registry
}
Reactivity keys
Integrate with @effect/experimental’s Reactivity service to automatically invalidate queries when mutations occur:
import { Atom } from "@effect-atom/atom-react"
import { Effect } from "effect"
import { Reactivity } from "@effect/experimental"
const runtimeAtom = Atom.runtime(Layer.empty)
const updateUserAtom = runtimeAtom.fn(
Effect.fnUntraced(function* (userId: string, name: string) {
// Perform mutation
yield* Effect.log("Updating user", userId, name)
}),
// Automatically invalidate these keys when the mutation completes
{ reactivityKeys: ["users", `user:${userId}`] }
)
const userQueryAtom = Atom.make(() => fetchUser(userId)).pipe(
// Refresh when these keys are invalidated
Atom.withReactivity(["users", `user:${userId}`])
)
Stream functions
Function atoms can also return streams:
import { Atom } from "@effect-atom/atom-react"
import { Effect, Stream } from "effect"
const streamFn = Atom.fn(
Effect.fnUntraced(function* (count: number) {
return Stream.range(0, count).pipe(
Stream.tap((n) => Effect.log(`Emitting ${n}`))
)
})
)
// The Result will contain the last emitted value from the stream
When a function atom returns a stream, the atom’s value is updated with the last emitted value from each chunk, similar to how Atom.make handles streams.
Synchronous functions
For synchronous operations that don’t need Effect, use Atom.fnSync:
import { Atom } from "@effect-atom/atom-react"
const addAtom = Atom.fnSync((n: number, get: Atom.FnContext) => {
const multiplier = get(multiplierAtom)
return n + multiplier
})
function Component() {
const result = useAtomValue(addAtom) // Option.Option<number>
const add = useAtomSet(addAtom)
return (
<button onClick={() => add(5)}>
Add 5
</button>
)
}
With initial value:
const addAtom = Atom.fnSync(
(n: number) => n + 1,
{ initialValue: 0 }
)
// Result is number (not Option<number>)
Best practices
Use function atoms for operations rather than state. If you’re storing data that changes over time, use a regular atom. Use function atoms for actions like API calls, mutations, or side effects.
Naming conventions
// ✅ Good: Action-oriented names
const createUserAtom = Atom.fn(...)
const deletePostAtom = Atom.fn(...)
const sendMessageAtom = Atom.fn(...)
// ❌ Bad: State-oriented names
const userAtom = Atom.fn(...)
const postAtom = Atom.fn(...)
Error handling
Function atoms automatically capture errors in the Result:
const riskyOperation = Atom.fn(
Effect.fnUntraced(function* (input: string) {
if (!input) {
yield* Effect.fail(new Error("Input required"))
}
return input.toUpperCase()
})
)
function Component() {
const [result, execute] = useAtom(riskyOperation)
return Result.builder(result)
.onInitial(() => <button onClick={() => execute("hello")}>Execute</button>)
.onSuccess((value) => <p>Result: {value}</p>)
.onFailure((cause) => (
<div>
<p>Error: {Cause.pretty(cause)}</p>
<button onClick={() => execute("hello")}>Retry</button>
</div>
))
.render()
}
Combining with atom families
Create parameterized function atoms using families:
const deletePostFamily = Atom.family((userId: string) =>
runtimeAtom.fn(
Effect.fnUntraced(function* (postId: string) {
const posts = yield* PostsService
return yield* posts.delete(userId, postId)
})
)
)
function DeleteButton({ userId, postId }: Props) {
const deletePost = useAtomSet(deletePostFamily(userId), {
mode: "promiseExit"
})
return (
<button onClick={() => deletePost(postId)}>
Delete
</button>
)
}