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 withAsyncData extension creates a properly typed data atom that stores the results of successful async operations. It includes all features of withAsync and withAbort for complete async handling. This extension is perfect for managing data fetching patterns where you need to:
  • Store fetched data in an atom
  • Track loading states
  • Handle errors
  • Abort outdated requests

Type Signature

// Basic usage - data type inferred from payload
function withAsyncData<Err = Error, EmptyErr = undefined>(
  options?: AsyncOptions<Err, EmptyErr>
): <T extends AtomLike>(
  target: T
) => T extends AtomLike<any, infer Params, Promise<infer Payload>>
  ? AsyncDataExt<Params, Payload, Payload, undefined, Err | EmptyErr>
  : never

// With initial state matching payload type
function withAsyncData<T extends AtomLike, Err = Error, EmptyErr = undefined>(
  options: AsyncOptions<Err, EmptyErr> & {
    initState: Payload
    mapPayload?: (payload: Payload, params: Params, state: Payload) => Payload
  }
): (target: T) => AsyncDataExt<Params, Payload, Payload, Payload, Err | EmptyErr>

// With custom state type and mapper
function withAsyncData<State, T extends AtomLike, Err = Error, EmptyErr = undefined>(
  options: AsyncOptions<Err, EmptyErr> & {
    initState: State
    mapPayload: (payload: Payload, params: Params, state: State) => State
  }
): (target: T) => AsyncDataExt<Params, Payload, State, State, Err | EmptyErr>

Parameters

options
AsyncDataOptions
Configuration options extending AsyncOptions

Return Value

Returns the target extended with all AsyncExt properties plus:
data
Atom<InitState | State>
Atom that stores the fetched data. Updated automatically when async operations complete successfully.Includes a reset action to reset data to initial state.
abort
Action<[reason?: any]>
Action to abort ongoing async operations (from withAbort)
reset
Action<[], void>
Action that resets dependencies and data to initial state. Does not automatically re-fetch.
status
AsyncStatusAtom<State, InitState>
Status atom that includes the data property (when status: true)

Examples

Basic Data Fetching with Computed

import { atom, computed } from '@reatom/core'
import { withAsyncData } from '@reatom/core/async'
import { wrap } from '@reatom/core/methods'

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 the fetched data
userData.data()   // → the user data or undefined
userData.error()  // → error if fetch failed
userData.ready()  // → false while loading, true when complete

With Initial State

interface Todo {
  id: number
  title: string
  completed: boolean
}

const todoId = atom(1, 'todoId')

const todoData = computed(async () => {
  const id = todoId()
  const res = await wrap(fetch(`/api/todos/${id}`))
  return await wrap(res.json()) as Todo
}, 'todoData').extend(
  withAsyncData({
    initState: { id: 0, title: '', completed: false },
  })
)

// Data is never undefined
todoData.data() // → { id: 0, title: '', completed: false } initially

Accumulating Data with mapPayload

const fetchPage = action(async (page: number) => {
  const res = await wrap(fetch(`/api/items?page=${page}`))
  return await wrap(res.json())
}, 'fetchPage').extend(
  withAsyncData({
    initState: [] as Item[],
    mapPayload: (newItems, [page], currentItems) => {
      // Append new items to existing ones
      return [...currentItems, ...newItems]
    },
  })
)

// Load multiple pages
await wrap(fetchPage(1))
await wrap(fetchPage(2))
await wrap(fetchPage(3))

// All items accumulated
fetchPage.data() // → [...page1Items, ...page2Items, ...page3Items]

Error Handling with Custom Type

interface ApiError {
  code: string
  message: string
}

const resource = computed(async () => {
  const res = await wrap(fetch('/api/resource'))
  if (!res.ok) {
    throw await res.json() // API returns { code, message }
  }
  return await wrap(res.json())
}, 'resource').extend(
  withAsyncData<ApiError>({
    parseError: (error) => {
      if (typeof error === 'object' && error !== null) {
        return error as ApiError
      }
      return { code: 'UNKNOWN', message: String(error) }
    },
    initState: null,
  })
)

const error = resource.error()
if (error) {
  console.log(`Error ${error.code}: ${error.message}`)
}

Concurrent Request Handling

const fetchItem = action(async (id: number) => {
  await wrap(sleep())
  return { id, data: `Item ${id}` }
}, 'fetchItem').extend(withAsyncData({ initState: null }))

// Start multiple concurrent requests
fetchItem(1)
fetchItem(2)
fetchItem(3)

await wrap(sleep())

// Only the last request updates the data (last-in-win)
fetchItem.data() // → { id: 3, data: 'Item 3' }

Reset and Re-fetch Pattern

const param = atom(1, 'param')

const resource = computed(async () => {
  const value = param()
  await wrap(sleep())
  return value * 10
}, 'resource').extend(withAsyncData({ initState: 0 }))

resource.data.subscribe()
await wrap(sleep())
console.log(resource.data()) // → 10

// Reset clears data but doesn't re-fetch
resource.reset()
console.log(resource.data()) // → 0

// Manually trigger re-fetch
await wrap(resource())
console.log(resource.data()) // → 10

Retry After Error

let shouldFail = true

const resource = computed(async () => {
  await wrap(sleep())
  if (shouldFail) throw new Error('Test error')
  return 'Success'
}, 'resource').extend(withAsyncData())

resource()
await wrap(sleep())

console.log(resource.error())        // → Error: Test error
console.log(resource.data())         // → undefined

// Fix the condition and retry
shouldFail = false
await wrap(resource.retry())

console.log(resource.error())        // → undefined
console.log(resource.data())         // → 'Success'

With Status Including Data

const fetch = action(
  async (param: number) => param + 1,
  'fetch'
).extend(withAsyncData({ status: true }))

const status = fetch.status()
console.log(status.data)         // → undefined
console.log(status.isPending)    // → false

const promise = fetch(5)
const pendingStatus = fetch.status()
console.log(pendingStatus.data)        // → undefined
console.log(pendingStatus.isPending)   // → true

await wrap(promise)
const fulfilledStatus = fetch.status()
console.log(fulfilledStatus.data)       // → 6
console.log(fulfilledStatus.isFulfilled) // → true

Use Cases

User Profile Management

const currentUserId = atom<string | null>(null, 'currentUserId')

const userProfile = computed(async () => {
  const userId = currentUserId()
  if (!userId) return null
  
  const res = await wrap(fetch(`/api/users/${userId}`))
  return await wrap(res.json())
}, 'userProfile').extend(
  withAsyncData({ initState: null })
)

// Load user
currentUserId.set('user-123')

// Show loading state
if (!userProfile.ready()) {
  console.log('Loading...')
}

// Show data
const profile = userProfile.data()
if (profile) {
  console.log(`Welcome, ${profile.name}!`)
}

Pagination with History

const currentPage = atom(1, 'currentPage')
const pageSize = atom(20, 'pageSize')

const items = computed(async () => {
  const page = currentPage()
  const size = pageSize()
  const res = await wrap(fetch(`/api/items?page=${page}&size=${size}`))
  return await wrap(res.json())
}, 'items').extend(
  withAsyncData({
    initState: [] as Item[],
  })
)

// Load first page
await wrap(items())

// Navigate to next page - previous data is replaced
currentPage.set(2)
await wrap(items())

Build docs developers (and LLMs) love