Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The withSuspense extension adds suspense support to async atoms by creating a suspended computed atom that tracks resolved values from promises. It’s designed to work seamlessly with React Suspense boundaries. The suspended atom will:
  • Return the resolved value immediately if the promise is already fulfilled
  • Throw the promise if it’s still pending (allowing Suspense boundaries to catch it)
  • Propagate errors if the promise is rejected
  • Automatically update when the promise resolves

Type Signature

function withSuspense<Target extends AtomLike>(options?: {
  preserve?: boolean
}): Ext<Target, SuspenseExt<AtomState<Target>>>

interface SuspenseExt<State> {
  suspended: Computed<Awaited<State>>
}

function suspense<State>(target: AtomLike<State>): Awaited<State>

function settled<Result, Fallback = undefined>(
  promise: Result | Promise<Result>,
  fallback?: Fallback
): Result | Fallback

function withSuspenseInit<State>(): Ext<Atom<Promise<State>>, Atom<State>>
function withSuspenseInit<Target extends AtomLike>(
  cb: (state?: AtomState<Target>) => AtomState<Target> | Promise<AtomState<Target>>
): Ext<Target>

Parameters

options
object

Return Value

suspended
Computed<Awaited<State>>
Computed atom that returns the resolved value or throws the promise/error

Examples

Basic Suspense

import { computed, atom } from '@reatom/core'
import { withSuspense, suspense } from '@reatom/core/extensions'
import { wrap } from '@reatom/core/methods'

const param = atom(0, 'param')
const data = computed(async () => param(), 'data')

const result = computed(() => {
  const syncData = suspense(data) // Auto-applies withSuspense
  return syncData
}, 'result')

const track = subscribe(
  computed(() => {
    try {
      return result()
    } catch (error) {
      return undefined // Handle pending state
    }
  })
)

await wrap(sleep())
console.log(track) // Called with: 0

param.set(1)
// Throws promise while pending

await wrap(sleep())
console.log(result()) // → 1

Using withSuspense Extension

const param = atom(0, 'param')
const data = computed(
  async () => param(),
  'data'
).extend(withSuspense())

const track = subscribe(data.suspended)
await wrap(sleep())
console.log(track) // Called with: 0

param.set(1)
await wrap(sleep())
console.log(track) // Called with: 1

React Suspense Integration

import { computed } from '@reatom/core'
import { withSuspense } from '@reatom/core/extensions'
import { useAtom } from '@reatom/react'

const userData = computed(async () => {
  const res = await fetch('/api/user')
  return res.json()
}, 'userData').extend(withSuspense())

function UserProfile() {
  // Throws promise if pending, renders when resolved
  const user = useAtom(userData.suspended)
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile />
    </Suspense>
  )
}

Error Handling with Suspense

const param = atom(0, 'param')
const data = computed(async () => {
  const value = param()
  if (value < 5) throw new Error('Value too low')
  return value
}, 'data')

let calls = 0
const result = computed(() => {
  try {
    calls++
    return suspense(data)
  } catch (error) {
    return error
  }
}, 'result')

const track = subscribe(result)

// First call - pending
console.log(track.mock.lastCall[0]) // → Promise

await wrap(sleep())

// Second call - rejected
console.log(track.mock.lastCall[0]) // → Error: Value too low

param.set(10)
await wrap(sleep())

// Third call - fulfilled
console.log(track.mock.lastCall[0]) // → 10

Preserve Previous State

const data = computed(
  async () => {
    await wrap(sleep(100))
    return Math.random()
  },
  'data'
).extend(withSuspense({ preserve: true }))

const track = subscribe(data.suspended)
await wrap(sleep())
const firstValue = track.mock.lastCall[0]

// Trigger re-fetch
data()

// With preserve: true, keeps showing first value
// instead of throwing promise
const duringFetch = data.suspended()
console.log(duringFetch === firstValue) // → true

await wrap(sleep())
const secondValue = track.mock.lastCall[0]
console.log(secondValue !== firstValue) // → true

withSuspenseInit

Enables asynchronous initialization for synchronous atoms:

Unwrap Promise Type

const data = atom(async () => {
  await sleep()
  return { value: 42 }
}).extend(withSuspenseInit())

// Type: Atom<{ value: number }> (not Atom<Promise<{ value: number }>>)

try {
  data() // Throws promise on first call
} catch (promise) {
  await wrap(promise)
}

console.log(data()) // → { value: 42 }

Async Initializer Callback

const todos = atom<Todo[]>([]).extend(
  withSuspenseInit(async () => {
    const cached = await indexedDB.get('todos')
    return cached ?? []
  }),
  withChangeHook((newState) => {
    // Sync changes back to storage
    indexedDB.set('todos', newState)
  })
)

// Local-first pattern:
// 1. Async load from IndexedDB on init
// 2. Sync operations after init
// 3. Auto-sync changes back to IndexedDB

Typed Async Init

const profile = atom<{ username: string; age: number }>({
  username: 'guest',
  age: 0,
}).extend(
  withSuspenseInit(async () => {
    const data = await fetchProfile()
    return data ?? { username: 'guest', age: 0 }
  })
)

settled Helper

Check if a promise is settled and get its value:
import { settled } from '@reatom/core/extensions'

const promise = Promise.resolve(42)
await promise

const value = settled(promise)
console.log(value) // → 42

const pending = new Promise(() => {})
const fallback = settled(pending, 'loading')
console.log(fallback) // → 'loading'

suspense Helper

Automatically apply withSuspense and get suspended value:
import { suspense } from '@reatom/core/extensions'

const data = computed(async () => {
  const res = await fetch('/api/data')
  return res.json()
}, 'data')

const result = computed(() => {
  try {
    return suspense(data) // Auto-applies withSuspense
  } catch (promise) {
    if (promise instanceof Promise) {
      return undefined // Handle pending
    }
    throw promise // Re-throw errors
  }
}, 'result')

Use Cases

Data Fetching with Suspense

const userId = atom('1', 'userId')

const user = computed(async () => {
  const id = userId()
  const res = await fetch(`/api/users/${id}`)
  return res.json()
}, 'user').extend(withSuspense())

function UserCard() {
  const data = useAtom(user.suspended)
  return <div>{data.name}</div>
}

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <UserCard />
    </Suspense>
  )
}

Conditional Suspense

const suspenseAtom = atom(async () => {
  await wrap(sleep())
  return true
}).extend(withSuspenseInit())

const otherSuspenseAtom = atom(async () => {
  await wrap(sleep())
  return false
}).extend(withSuspenseInit())

const suspenseProxyDep = atom(0)

const suspenseProxy = computed(() => {
  suspenseProxyDep()
  return suspenseProxyDep() 
    ? otherSuspenseAtom() 
    : suspenseAtom()
})

const component = computed(() => suspenseProxy())

// Helper to retry suspense
const suspenseRetry = async (cb: () => unknown) => {
  while (true) {
    try {
      return cb()
    } catch (error) {
      if (error instanceof Promise) {
        await wrap(error)
      } else {
        throw error
      }
    }
  }
}

await suspenseRetry(component) // → true
suspenseProxyDep.set(1)
await suspenseRetry(component) // → false

Local-First Pattern

const userSettings = atom(async () => {
  const stored = await localforage.getItem('settings')
  return stored ?? defaultSettings
}).extend(
  withSuspenseInit(),
  withChangeHook((settings) => {
    // Auto-save changes
    localforage.setItem('settings', settings)
  })
)

// Type: Atom<Settings> (not Atom<Promise<Settings>>)
// After init, all operations are synchronous
effect(() => {
  const settings = userSettings()
  console.log(settings.theme)
})

Progressive Enhancement

const data = computed(async () => {
  await sleep(1000)
  return { items: [...Array(100)] }
}, 'data').extend(withSuspense({ preserve: true }))

function DataView() {
  const value = useAtom(data.suspended)
  
  // With preserve: true, old data stays visible
  // while new data loads
  return (
    <div>
      {value.items.map(item => <Item {...item} />)}
    </div>
  )
}