Skip to content

Instantly share code, notes, and snippets.

@tim-smart
Created August 14, 2026 06:54
Show Gist options
  • Select an option

  • Save tim-smart/e59ab9b40d8daa23f6e6300042f3ba06 to your computer and use it in GitHub Desktop.

Select an option

Save tim-smart/e59ab9b40d8daa23f6e6300042f3ba06 to your computer and use it in GitHub Desktop.
Effect Atom + React data fetching examples (Effect v4 / @effect/atom-react)

Effect Atom + React: data fetching (Effect v4)

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.

Install

pnpm add effect@rc @effect/atom-react@rc
# React 19 peer
pnpm add react@^19 scheduler@^0.27

v3 → v4 map (quick)

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)

1. Minimal fetch atom

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.tryPromise accepts an AbortSignal in the try callback 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) — use result.waiting for background refresh state.

2. Service + Atom.runtime (recommended)

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>
  )
}

Parameterized fetch with Atom.family

// 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>
  )
}

3. Mutations with runtime.fn

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>
  )
}

4. Infinite scroll / pagination with Atom.pull

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()
}

5. App shell: RegistryProvider

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>
  )
}

6. Suspense alternative

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>
  )
}

Mental model

  • 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 via RegistryContext

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment