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.

Async Operations

Reatom provides powerful utilities for managing async operations with built-in state tracking, error handling, and automatic abort capabilities.

Core Concepts

Reatom’s async extensions transform atoms and actions into powerful async state machines with:
  • Pending state tracking - Know when operations are in progress
  • Error handling - Automatic error capture and parsing
  • Status tracking - Detailed lifecycle state information
  • Data caching - Store and access the latest successful results
  • Abort control - Cancel pending operations automatically

Basic Async Actions

Use withAsync() to add async capabilities to any action that returns a promise:
import { action, wrap } from '@reatom/core'
import { withAsync } from '@reatom/core/async'

const fetchUser = action(async (userId: string) => {
  const response = await wrap(fetch(`/api/users/${userId}`))
  return await wrap(response.json())
}, 'fetchUser').extend(withAsync())

// Track loading state
fetchUser.ready()    // → true when complete, false when pending
fetchUser.pending()  // → number of pending operations
fetchUser.error()    // → latest error if any
Always use wrap() around promises to ensure proper context binding and abort handling.

Async Data Management

withAsyncData() extends withAsync() with automatic data storage:
import { computed, atom } from '@reatom/core'
import { withAsyncData, wrap } from '@reatom/core'

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

const userData = computed(async () => {
  const id = userId()
  const response = await wrap(fetch(`/api/users/${id}`))
  if (!response.ok) throw new Error('Failed to fetch user')
  return await wrap(response.json())
}, 'userData').extend(withAsyncData())

// Access fetched data and states
userData.data()   // → the fetched user data
userData.error()  // → error if fetch failed
userData.ready()  // → false while loading

With Initial State

Provide type-safe initial values:
const userData = computed(async () => {
  // ... fetch logic
  return { name: '', email: '' }
}, 'userData').extend(
  withAsyncData({ 
    initState: { name: '', email: '' } 
  })
)

// userData.data() is never undefined
console.log(userData.data().name) // ✓ Type-safe access

Status Tracking

Enable detailed status tracking with the status option:
1
Enable Status
2
const fetchUser = action(async (id: string) => {
  const res = await wrap(fetch(`/api/users/${id}`))
  return await wrap(res.json())
}, 'fetchUser').extend(withAsync({ status: true }))
3
Access Status Properties
4
const status = fetchUser.status()

// Current state flags
status.isPending     // Operation in progress
status.isFulfilled   // Last operation succeeded
status.isRejected    // Last operation failed
status.isSettled     // Operation completed

// Historical tracking
status.isFirstPending  // First-ever operation
status.isEverPending   // At least one operation started
status.isEverSettled   // At least one operation completed
5
Use for Conditional UI
6
function UserProfile() {
  const status = fetchUser.status()
  
  if (status.isFirstPending) {
    return <Skeleton /> // Show skeleton only on first load
  }
  
  if (status.isPending) {
    return <Spinner /> // Show spinner on subsequent loads
  }
  
  if (status.isRejected) {
    return <ErrorMessage error={fetchUser.error()} />
  }
  
  return <UserData data={fetchUser.data()} />
}

Error Handling

Custom Error Parsing

Transform errors into a consistent format:
interface ApiError {
  message: string
  code: number
  field?: string
}

const fetchUser = action(async (id: string) => {
  const res = await wrap(fetch(`/api/users/${id}`))
  if (!res.ok) {
    const error = await wrap(res.json())
    throw error
  }
  return await wrap(res.json())
}, 'fetchUser').extend(
  withAsync<ApiError>({
    parseError: (error: unknown) => {
      if (error && typeof error === 'object' && 'code' in error) {
        return error as ApiError
      }
      return { message: String(error), code: 500 }
    }
  })
)

// Type-safe error access
const error = fetchUser.error()
if (error) {
  console.log(error.code, error.message) // ✓ Typed as ApiError
}

Error Reset Strategies

Clear errors when the operation starts:
const action = action(async () => {
  // ...
}).extend(withAsync({ resetError: 'onCall' }))

// Error is cleared immediately when called
await action()

Retry Logic

Retry Actions

Enable parameter caching to retry actions:
const fetchUser = action(async (userId: string) => {
  const res = await wrap(fetch(`/api/users/${userId}`))
  return await wrap(res.json())
}, 'fetchUser').extend(
  withAsync({ cacheParams: true })
)

// Call with params
await wrap(fetchUser('123'))

// Retry with cached params
await wrap(fetchUser.retry())

Retry Computed Atoms

Computed atoms can retry without caching:
import { retryComputed } from '@reatom/core/methods'

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

// First call fails
await wrap(userData().catch(noop))

// Retry the computation
await wrap(retryComputed(userData))

// Or use the retry method
await wrap(userData.retry())

Lifecycle Hooks

React to async events with lifecycle actions:
import { withCallHook } from '@reatom/core'

const fetchUser = action(async (id: string) => {
  const res = await wrap(fetch(`/api/users/${id}`))
  return await wrap(res.json())
}, 'fetchUser').extend(withAsync())

// On successful completion
fetchUser.onFulfill.extend(
  withCallHook(({ payload, params }) => {
    console.log('Fetched user:', payload)
    console.log('With ID:', params[0])
  })
)

// On error
fetchUser.onReject.extend(
  withCallHook(({ error, params }) => {
    console.error('Failed to fetch user:', params[0])
    console.error('Error:', error)
  })
)

// On completion (success or error)
fetchUser.onSettle.extend(
  withCallHook((result) => {
    if ('payload' in result) {
      console.log('Success:', result.payload)
    } else {
      console.log('Error:', result.error)
    }
  })
)

Concurrent Operations

Pending Counter

Track multiple concurrent operations:
const fetchUser = action(async (id: string) => {
  await wrap(sleep(1000))
  return { id, name: 'User' }
}, 'fetchUser').extend(withAsync())

// Start multiple operations
fetchUser('1')
fetchUser('2')
fetchUser('3')

console.log(fetchUser.pending()) // → 3

// Wait for all to complete
await wrap(sleep(1100))
console.log(fetchUser.pending()) // → 0
console.log(fetchUser.ready())   // → true

Automatic Abort

Async operations are automatically aborted when:
  • The atom’s dependencies change (for computed atoms)
  • A new operation starts (for actions with abort)
  • The component unmounts (in React/framework integrations)
import { withAbort } from '@reatom/core'

const searchQuery = atom('', 'searchQuery')

const searchResults = computed(async () => {
  const query = searchQuery()
  // Automatically aborted when searchQuery changes
  const res = await wrap(fetch(`/api/search?q=${query}`))
  return await wrap(res.json())
}, 'searchResults').extend(withAsyncData())

// When searchQuery changes, previous fetch is aborted
searchQuery.set('react')  // Starts fetch 1
searchQuery.set('reatom') // Aborts fetch 1, starts fetch 2

Best Practices

Always use wrap() around promises - This ensures proper context binding and enables automatic abort handling.

1. Use withAsyncData for Data Fetching

When you need to store results, use withAsyncData() instead of withAsync():
// ✓ Good - stores data automatically
const userData = computed(async () => {
  return await wrap(fetchUser())
}).extend(withAsyncData())

// ✗ Bad - no data storage
const userData = computed(async () => {
  return await wrap(fetchUser())
}).extend(withAsync())

2. Enable Status for Complex UIs

Use status tracking when you need fine-grained loading states:
const fetchData = action(async () => {
  // ...
}).extend(withAsync({ status: true }))

// Differentiate first load from subsequent loads
if (fetchData.status().isFirstPending) {
  return <Skeleton />
}

3. Handle Errors Gracefully

Always provide error handling:
import { noop } from '@reatom/core'

// Catch errors to prevent unhandled rejections
await wrap(fetchUser()).catch(noop)

// Or handle errors in UI
if (fetchUser.error()) {
  return <ErrorMessage error={fetchUser.error()} />
}

4. Use Custom Error Types

Define consistent error shapes:
interface AppError {
  message: string
  code: string
  retryable: boolean
}

const fetchData = action(async () => {
  // ...
}).extend(
  withAsync<AppError>({
    parseError: (error) => ({
      message: String(error),
      code: 'UNKNOWN',
      retryable: true
    })
  })
)

Common Patterns

Polling

import { effect, wrap, sleep } from '@reatom/core'

const pollInterval = atom(5000, 'pollInterval')

const pollData = action(async () => {
  while (true) {
    const res = await wrap(fetch('/api/status'))
    const data = await wrap(res.json())
    dataAtom.set(data)
    await wrap(sleep(pollInterval()))
  }
}, 'pollData').extend(withAbort())

// Start polling
effect(() => {
  pollData()
})

Optimistic Updates

const updateUser = action(async (updates: Partial<User>) => {
  // Optimistically update
  const prevUser = userAtom()
  userAtom.set({ ...prevUser, ...updates })
  
  try {
    const res = await wrap(fetch('/api/user', {
      method: 'PATCH',
      body: JSON.stringify(updates)
    }))
    const updated = await wrap(res.json())
    userAtom.set(updated)
    return updated
  } catch (error) {
    // Revert on error
    userAtom.set(prevUser)
    throw error
  }
}, 'updateUser').extend(withAsync())

Sequential Operations

const setupAccount = action(async (email: string) => {
  // Step 1: Create user
  const user = await wrap(createUser(email))
  
  // Step 2: Send verification email
  await wrap(sendVerificationEmail(user.id))
  
  // Step 3: Create default settings
  await wrap(createSettings(user.id))
  
  return user
}, 'setupAccount').extend(
  withAsyncData({ status: true })
)

// Track progress through status
const status = setupAccount.status()
if (status.isPending) {
  return <Progress />
}