Skip to main content
effect-atom provides first-class React integration for Effect.ts, making it easy to use voltaire-effect in React applications with automatic dependency injection, caching, and reactive updates.

Installation

npm install @effect-atom/atom @effect-atom/atom-react effect voltaire-effect

Setup

Wrap your app with RegistryProvider to enable atom reactivity:
import { RegistryProvider } from "@effect-atom/atom-react"

function App() {
  return (
    <RegistryProvider>
      <YourApp />
    </RegistryProvider>
  )
}

Creating a Runtime with Layers

Use Atom.runtime to create a runtime pre-configured with your Provider layer:
import { Atom } from "@effect-atom/atom"
import { Layer } from "effect"
import { HttpProvider } from "voltaire-effect"

// Create runtime with composed layers
const runtime = Atom.runtime(
  HttpProvider("https://eth.llamarpc.com")
)

Creating Atoms

Atoms wrap Effect programs and make them reactive:
import { Atom } from "@effect-atom/atom"
import { getBalance, getBlockNumber } from "voltaire-effect"

// Simple atom - fetches latest block number
const blockNumberAtom = runtime.atom(getBlockNumber)

// Parameterized atom factory - returns a new atom per address
const balanceAtom = (address: `0x${string}`) =>
  runtime.atom(getBalance(address, "latest"))

Using Atoms in Components

Basic Usage with useAtomValue

import { useAtomValue, Result } from "@effect-atom/atom-react"

function Balance({ address }: { address: `0x${string}` }) {
  const result = useAtomValue(balanceAtom(address))

  return Result.match(result, {
    onInitial: () => <div>Loading...</div>,
    onSuccess: (balance) => (
      <div>{(Number(balance) / 1e18).toFixed(4)} ETH</div>
    ),
    onFailure: (cause) => (
      <div>Error: {cause._tag}</div>
    ),
  })
}

Result Builder Pattern

For more complex rendering, use the builder pattern:
import { Result } from "@effect-atom/atom-react"

function BlockNumber() {
  const result = useAtomValue(blockNumberAtom)

  return Result.builder(result)
    .onInitial(() => <span className="loading">...</span>)
    .onSuccess((block) => <span>Block #{block.toString()}</span>)
    .onFailure((cause) => <span className="error">Failed</span>)
    .render()
}

Result Type

The Result type represents async operation states:
StateDescriptionProperties
InitialEffect hasn’t started yetwaiting: boolean
SuccessEffect completed successfullyvalue: A
FailureEffect failedcause: Cause<E>

Result Refinements

import { Result } from "@effect-atom/atom"

if (Result.isInitial(result)) {
  // Initial state - show loading
}

if (Result.isSuccess(result)) {
  console.log(result.value) // Access the value
}

if (Result.isFailure(result)) {
  console.log(Result.cause(result)) // Access error cause
}

if (Result.isWaiting(result)) {
  // Re-fetching with previous value available
}

Result Accessors

// Get value or default
const balance = Result.getOrElse(result, () => 0n)

// Get value or throw
const balance = Result.getOrThrow(result)

// Map over success value
const formatted = Result.map(result, (balance) =>
  (Number(balance) / 1e18).toFixed(4)
)

React Hooks Reference

useAtomValue

Read the current value of an atom reactively:
import { useAtomValue } from "@effect-atom/atom-react"

function Component() {
  const result = useAtomValue(myAtom)
  // Re-renders when atom value changes
}

useAtomSet

Get a setter function for writable atoms:
import { useAtomSet } from "@effect-atom/atom-react"

function Component() {
  const setAddress = useAtomSet(addressAtom)
  
  return (
    <input onChange={(e) => setAddress(e.target.value)} />
  )
}

useAtom

Combined read and write access:
import { useAtom } from "@effect-atom/atom-react"

function Component() {
  const [value, setValue] = useAtom(myAtom)
}

useAtomRefresh

Manually trigger re-execution of an effectful atom:
import { useAtomRefresh, useAtomValue, Result } from "@effect-atom/atom-react"

function BalanceWithRefresh({ address }: { address: `0x${string}` }) {
  const result = useAtomValue(balanceAtom(address))
  const refresh = useAtomRefresh(balanceAtom(address))

  return (
    <div>
      {Result.match(result, {
        onInitial: () => "Loading...",
        onSuccess: (b) => `${(Number(b) / 1e18).toFixed(4)} ETH`,
        onFailure: () => "Error",
      })}
      <button onClick={refresh}>Refresh</button>
    </div>
  )
}

useAtomSuspense

Use with React Suspense for cleaner loading states:
import { useAtomSuspense } from "@effect-atom/atom-react"
import { Suspense } from "react"

function BalanceSuspense({ address }: { address: `0x${string}` }) {
  // Suspends until success, throws on failure
  const balance = useAtomSuspense(balanceAtom(address))
  
  return <div>{(Number(balance) / 1e18).toFixed(4)} ETH</div>
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <BalanceSuspense address="0x..." />
    </Suspense>
  )
}

Derived Atoms

Create atoms that derive from other atoms:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
import { getBalance, getBlockNumber } from "voltaire-effect"

// Derive from multiple atoms
const accountSummaryAtom = (address: `0x${string}`) =>
  runtime.atom(
    Effect.all({
      balance: getBalance(address, "latest"),
      blockNumber: getBlockNumber,
    })
  )

Multiple Chains

Create separate runtimes for different networks:
import { Atom } from "@effect-atom/atom"
import { HttpProvider } from "voltaire-effect"

const mainnetRuntime = Atom.runtime(
  HttpProvider("https://eth.llamarpc.com")
)

const optimismRuntime = Atom.runtime(
  HttpProvider("https://mainnet.optimism.io")
)

// Use in components
const mainnetBalance = mainnetRuntime.atom(getBalance(addr, "latest"))
const optimismBalance = optimismRuntime.atom(getBalance(addr, "latest"))

Next.js Setup

App Router

// app/providers.tsx
"use client"

import { RegistryProvider } from "@effect-atom/atom-react"

export function Providers({ children }: { children: React.ReactNode }) {
  return <RegistryProvider>{children}</RegistryProvider>
}
// app/layout.tsx
import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Atoms Module

// lib/atoms.ts
import { Atom } from "@effect-atom/atom"
import { HttpProvider, getBalance, getBlockNumber } from "voltaire-effect"

const runtime = Atom.runtime(
  HttpProvider(process.env.NEXT_PUBLIC_RPC_URL!)
)

export const blockNumberAtom = runtime.atom(getBlockNumber)

export const balanceAtom = (address: `0x${string}`) =>
  runtime.atom(getBalance(address, "latest"))

Using in Pages

// app/page.tsx
"use client"

import { useAtomValue, Result } from "@effect-atom/atom-react"
import { blockNumberAtom, balanceAtom } from "@/lib/atoms"

export default function Home() {
  const blockResult = useAtomValue(blockNumberAtom)
  const balanceResult = useAtomValue(
    balanceAtom("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
  )

  return (
    <main>
      <h1>Ethereum Dashboard</h1>
      
      <section>
        <h2>Latest Block</h2>
        {Result.match(blockResult, {
          onInitial: () => <p>Loading...</p>,
          onSuccess: (block) => <p>#{block.toString()}</p>,
          onFailure: () => <p>Error loading block</p>,
        })}
      </section>

      <section>
        <h2>Vitalik's Balance</h2>
        {Result.match(balanceResult, {
          onInitial: () => <p>Loading...</p>,
          onSuccess: (balance) => (
            <p>{(Number(balance) / 1e18).toFixed(4)} ETH</p>
          ),
          onFailure: () => <p>Error loading balance</p>,
        })}
      </section>
    </main>
  )
}

Advanced: Scoped Effects with Cleanup

Atoms automatically manage Effect scopes, running finalizers when unmounted:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
import { watchBlocks, HttpProvider } from "voltaire-effect"

// Block subscription with automatic cleanup
const blockStreamAtom = runtime.atom(
  Effect.gen(function* () {
    const stream = yield* watchBlocks({ includeTransactions: false })
    
    // Add cleanup when atom is no longer used
    yield* Effect.addFinalizer(() =>
      Effect.sync(() => console.log("Cleaned up block subscription"))
    )
    
    return stream
  })
)

Error Handling

Handle errors using Result pattern matching:
import { Cause } from "effect"

function ErrorAwareBalance({ address }: { address: `0x${string}` }) {
  const result = useAtomValue(balanceAtom(address))

  return Result.match(result, {
    onInitial: () => <LoadingSpinner />,
    onSuccess: (balance) => <BalanceDisplay balance={balance} />,
    onFailure: (cause) => {
      // Extract error details from Cause
      const error = Cause.squash(cause)
      
      if (error._tag === "TransportError") {
        return <div>Network error - check connection</div>
      }
      
      return <div>Unknown error: {String(error)}</div>
    },
  })
}

Best Practices

Atom MemoizationParameterized atom factories return new atoms each call. Memoize when needed:
const balanceAtoms = new Map<string, ReturnType<typeof balanceAtom>>()

function getBalanceAtom(address: `0x${string}`) {
  if (!balanceAtoms.has(address)) {
    balanceAtoms.set(address, balanceAtom(address))
  }
  return balanceAtoms.get(address)!
}
Server Componentseffect-atom hooks require client components. Use "use client" directive in Next.js App Router.

Resources