Skip to main content
The async list machine manages lists of items that are fetched asynchronously. It handles loading and error states, supports paginated data, server- and client-side sorting, and filtering. It is typically paired with the Combobox or Select component.
Inspired by React Stately’s useAsyncList.
Features
  • Support for pagination, sorting, and filtering
  • Abortable requests via AbortSignal
  • Automatic loading and error state management
  • External dependency tracking for reactive reloads

Installation

npm install @zag-js/async-list

Usage

Import the package and wire it into your framework’s state machine adapter.
import * as asyncList from "@zag-js/async-list"
The package exports two key functions:
  • machine — the state machine logic for managing async data.
  • connect — returns the properties and methods for interacting with the data.
import * as asyncList from "@zag-js/async-list"
import { useMachine } from "@zag-js/react"

function PokemonList() {
  const service = useMachine(asyncList.machine, {
    async load({ signal }) {
      const res = await fetch("https://pokeapi.co/api/v2/pokemon", { signal })
      const json = await res.json()
      return { items: json.results }
    },
  })

  const api = asyncList.connect(service)

  if (api.loading) return <p>Loading...</p>
  if (api.error) return <p>Error: {api.error.message}</p>

  return (
    <ul>
      {api.items.map((item) => (
        <li key={item.name}>{item.name}</li>
      ))}
    </ul>
  )
}

Loading and error states

The api exposes loading and error properties that reflect the current machine state.
api.loading  // true while fetching
api.error    // the Error instance if the last fetch failed, otherwise undefined
api.items    // the list items after a successful fetch
api.empty    // true when items.length === 0

Pagination

Return a cursor from your load function alongside items. The machine stores the cursor and passes it back on the next loadMore call. When no cursor is returned, api.hasMore is false.
const service = useMachine(asyncList.machine, {
  async load({ signal, cursor }) {
    const url = cursor ?? "https://pokeapi.co/api/v2/pokemon"
    const res = await fetch(url, { signal })
    const json = await res.json()
    return {
      items: json.results,
      cursor: json.next,
    }
  },
})

// Load the next page
api.loadMore()

Reloading data

Call api.reload() to clear the current items and re-run the load function from the beginning.
api.reload()

Sorting

Trigger a sort by calling api.sort() with a descriptor. You can choose between client-side and server-side sorting.
api.sort({ column: "name", direction: "ascending" })

Client-side sorting

Provide a sort function to perform sorting locally on the existing items.
const service = useMachine(asyncList.machine, {
  async load({ signal }) {
    const res = await fetch("https://example.com/api/items", { signal })
    const json = await res.json()
    return { items: json.results }
  },
  sort({ items, descriptor }) {
    return {
      items: items.sort((a, b) => {
        const aVal = a[descriptor.column]
        const bVal = b[descriptor.column]
        let cmp = String(aVal).localeCompare(String(bVal))
        if (descriptor.direction === "descending") cmp *= -1
        return cmp
      }),
    }
  },
})

Server-side sorting

Use the sortDescriptor parameter inside load to pass sorting parameters to your API endpoint.
const service = useMachine(asyncList.machine, {
  async load({ signal, sortDescriptor }) {
    const url = new URL("https://example.com/api/items")
    if (sortDescriptor) {
      url.searchParams.set("sort_key", String(sortDescriptor.column))
      url.searchParams.set("sort_direction", sortDescriptor.direction)
    }
    const res = await fetch(url, { signal })
    const json = await res.json()
    return { items: json.results }
  },
})

Filtering

Call api.setFilterText() to update the filter and trigger a new load. Use filterText inside load to pass the value to your API.
api.setFilterText("pikachu")
const service = useMachine(asyncList.machine, {
  async load({ signal, filterText }) {
    const url = new URL("https://example.com/api/items")
    if (filterText) url.searchParams.set("filter", filterText)
    const res = await fetch(url, { signal })
    const json = await res.json()
    return { items: json.results }
  },
})
To clear the filter, call api.clearFilter().
api.clearFilter()

Aborting requests

Pass the signal from load arguments to fetch. When api.abort() is called, the in-flight request is cancelled via AbortController.
const service = useMachine(asyncList.machine, {
  async load({ signal }) {
    const res = await fetch("https://example.com/api/items", { signal })
    const json = await res.json()
    return { items: json.results }
  },
})

// Abort the current in-flight fetch
api.abort()

Reacting to external dependencies

Use dependencies to register primitive values whose changes should trigger an automatic reload.
Each dependency must be a primitive value (string, number, boolean, null, or undefined). Objects, arrays, maps, and sets are not supported.
const service = useMachine(asyncList.machine, {
  dependencies: [userId],
  async load({ signal, deps }) {
    const res = await fetch(`https://example.com/api/items?user=${userId}`, { signal })
    const json = await res.json()
    return { items: json.results }
  },
})

API reference

Machine props

load
(args: LoadDetails<T, C>) => Promise<LoadResult<T, C>>
required
The async function called to fetch data. Receives signal, cursor, filterText, and sortDescriptor.
sort
(args: SortDetails<T>) => Promise<{ items: T[] }> | { items: T[] } | undefined
An optional function for client-side sorting. Receives the current items, descriptor, and filterText.
initialItems
T[]
Items to display before the first load call completes.
initialSortDescriptor
SortDescriptor<T>
The sort descriptor to apply on the first load.
initialFilterText
string
The filter text to use on the first load.
dependencies
LoadDependency[]
An array of primitive values. When any value changes, the list reloads automatically.
autoReload
boolean
When true, the machine triggers a reload when dependencies change.
onSuccess
(details: { items: T[] }) => void
Called after a successful fetch.
onError
(details: { error: Error }) => void
Called when the load function throws or rejects.

Connect API

Property / MethodTypeDescription
itemsT[]The current list of items.
filterTextstringThe current filter text.
cursorC | undefinedThe pagination cursor returned by the last fetch.
sortDescriptorSortDescriptor<T> | undefinedThe active sort descriptor.
loadingbooleantrue while fetching or sorting.
sortingbooleantrue while a client-side sort is in progress.
emptybooleantrue when items is empty.
hasMorebooleantrue when a non-null cursor is available.
errorany | undefinedThe error from the last failed fetch.
abort()() => voidCancels the current in-flight request.
reload()() => voidClears items and re-fetches from the beginning.
loadMore()() => voidFetches the next page using the stored cursor.
sort(descriptor)(SortDescriptor<T>) => voidApplies a sort descriptor and triggers a sort or reload.
setFilterText(text)(string) => voidUpdates the filter text and triggers a reload.
clearFilter()() => voidClears the filter text and triggers a reload.

Type definitions

export type SortDirection = "ascending" | "descending"

export interface SortDescriptor<T> {
  column: keyof T
  direction: SortDirection
}

export interface LoadDetails<T, C> {
  signal: AbortSignal | undefined
  filterText: string
  cursor?: C | undefined
  sortDescriptor?: SortDescriptor<T> | undefined
}

export interface LoadResult<T, C> {
  items: T[]
  cursor?: C | undefined
}

export type LoadDependency = string | number | boolean | undefined | null

Build docs developers (and LLMs) love