Examples for Effect v4 RC with the Atom modules that now live in core Effect,
plus React bindings from @effect/atom-react.
Verified against effect@4.0.0-rc.109 / @effect/atom-react@4.0.0-rc.109.
pnpm add effect@rc @effect/atom-react@rc
# React 19 peer
pnpm add react@^19 scheduler@^0.27| v3 | v4 |
|---|---|
@effect-atom/atom-react |
@effect/atom-react + effect/unstable/reactivity |
Result |
AsyncResult |
Result.builder |
AsyncResult.builder |
Effect.Service + .Default |
Context.Service + { make } + Layer.effect → .layer |
import { Atom, Result, useAtomValue } from "@effect-atom/atom-react" |
split imports (see below) |
import { Cause, Effect } from "effect"
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
import * as Atom from "effect/unstable/reactivity/Atom"
import { useAtomRefresh, useAtomValue } from "@effect/atom-react"
type User = { id: number; name: string }
// Effect atoms surface as AsyncResult
const usersAtom = Atom.make(
Effect.tryPromise({
try: async (signal) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users", {
signal,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return (await res.json()) as Array<User>
},
catch: (cause) => cause as Error,
}),
)
export function UsersList() {
const result = useAtomValue(usersAtom)
const refresh = useAtomRefresh(usersAtom)
return (
<div>
<button type="button" onClick={() => refresh()}>
Refresh
</button>
{AsyncResult.builder(result)
.onInitial(() => <p>Loading…</p>)
.onFailure((cause) => <p>Error: {Cause.pretty(cause)}</p>)
.onSuccess((users, r) => (
<div>
{r.waiting && <p>Refreshing…</p>}
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</div>
))
.orNull()}
</div>
)
}Notes:
Effect.tryPromiseaccepts anAbortSignalin thetrycallback so unmount/refresh can cancel in-flight work.AsyncResult.builder(...).orNull()is the v4 render end (also.render()still works as a method).onSuccess(value, result)— useresult.waitingfor background refresh state.
import { Cause, Context, Effect, Layer } from "effect"
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
import * as Atom from "effect/unstable/reactivity/Atom"
import { useAtomRefresh, useAtomValue } from "@effect/atom-react"
type User = { id: number; name: string }
class Users extends Context.Service<Users>()("app/Users", {
make: Effect.gen(function* () {
const getAll = Effect.tryPromise({
try: async (signal) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users", {
signal,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return (await res.json()) as Array<User>
},
catch: (cause) => cause as Error,
})
const findById = (id: string) =>
Effect.tryPromise({
try: async (signal) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
{ signal },
)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return (await res.json()) as User
},
catch: (cause) => cause as Error,
})
return { getAll, findById } as const
}),
}) {
// v4 does not auto-generate .Default — define the layer yourself
static readonly layer = Layer.effect(this, this.make)
}
// AtomRuntime from a Layer
const runtime = Atom.runtime(Users.layer)
const usersAtom = runtime.atom(
Effect.gen(function* () {
const users = yield* Users
return yield* users.getAll
}),
)
export function UsersList() {
const result = useAtomValue(usersAtom)
const refresh = useAtomRefresh(usersAtom)
return (
<div>
<button type="button" onClick={() => refresh()}>
Refresh
</button>
{AsyncResult.builder(result)
.onInitial(() => <p>Loading…</p>)
.onFailure((cause) => <p>Error: {Cause.pretty(cause)}</p>)
.onSuccess((users, r) => (
<div>
{r.waiting && <p>Refreshing…</p>}
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</div>
))
.orNull()}
</div>
)
}// stable atom per id
const userAtom = Atom.family((id: string) =>
runtime.atom(
Effect.gen(function* () {
const users = yield* Users
return yield* users.findById(id)
}),
),
)
export function UserProfile({ id }: { id: string }) {
const result = useAtomValue(userAtom(id))
const refresh = useAtomRefresh(userAtom(id))
return (
<div>
<button type="button" onClick={() => refresh()}>
Refresh
</button>
{AsyncResult.builder(result)
.onInitial(() => <p>Loading…</p>)
.onFailure((cause) => <p>Error: {Cause.pretty(cause)}</p>)
.onSuccess((user) => (
<h1>
{user.id}: {user.name}
</h1>
))
.orNull()}
</div>
)
}import { Exit } from "effect"
import { useAtomSet } from "@effect/atom-react"
const createUserAtom = runtime.fn(
Effect.fnUntraced(function* (name: string) {
const users = yield* Users
// stand-in create
return yield* Effect.succeed({ id: 1, name })
}),
)
export function CreateUserButton() {
// mode: "promiseExit" returns a Promise<Exit>
const createUser = useAtomSet(createUserAtom, { mode: "promiseExit" })
return (
<button
type="button"
onClick={async () => {
const exit = await createUser("Ada")
if (Exit.isSuccess(exit)) {
console.log(exit.value)
}
}}
>
Create user
</button>
)
}import { Cause, Stream } from "effect"
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
import * as Atom from "effect/unstable/reactivity/Atom"
import { useAtom } from "@effect/atom-react"
// pulls one chunk at a time; use runtime.pull when the stream needs services
const pagesAtom = Atom.pull(Stream.make(1, 2, 3, 4, 5))
export function PageList() {
const [result, pull] = useAtom(pagesAtom)
return AsyncResult.builder(result)
.onInitial(() => <p>Loading…</p>)
.onFailure((cause) => <p>Error: {Cause.pretty(cause)}</p>)
.onSuccess(({ items, done }, r) => (
<div>
<ul>
{items.map((n) => (
<li key={n}>{n}</li>
))}
</ul>
{!done && (
<button type="button" onClick={() => pull()}>
Load more
</button>
)}
{r.waiting && <p>Loading more…</p>}
</div>
))
.orNull()
}Hooks work against a default registry. Use RegistryProvider for SSR, tests, or
scoped disposal:
import { RegistryProvider } from "@effect/atom-react"
export function App() {
return (
<RegistryProvider>
<UsersList />
</RegistryProvider>
)
}import { useAtomSuspense } from "@effect/atom-react"
import { Suspense } from "react"
function UsersSuspense() {
// throws promise until Success; returns the success value
const users = useAtomSuspense(usersAtom).value
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
)
}
export function Page() {
return (
<Suspense fallback={<p>Loading…</p>}>
<UsersSuspense />
</Suspense>
)
}- Atom — reactive source of truth
- Effect / Stream atom — async work exposed as
AsyncResult - Atom.runtime(layer) — wires Effect services into atoms
- Hooks (
useAtomValue,useAtomRefresh,useAtom,useAtomSet) — React bridge viaRegistryContext
@effect/atom-react: https://www.npmjs.com/package/@effect/atom-react- Source: https://github.com/Effect-TS/effect/tree/main/packages/atom/react
- Atom core: https://github.com/Effect-TS/effect/blob/main/packages/effect/src/unstable/reactivity/Atom.ts
- Services migration: https://github.com/Effect-TS/effect/blob/main/migration/services.md