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
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
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.
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().
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.
Items to display before the first load call completes.
The sort descriptor to apply on the first load.
The filter text to use on the first load.
An array of primitive values. When any value changes, the list reloads automatically.
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 / Method | Type | Description |
|---|
items | T[] | The current list of items. |
filterText | string | The current filter text. |
cursor | C | undefined | The pagination cursor returned by the last fetch. |
sortDescriptor | SortDescriptor<T> | undefined | The active sort descriptor. |
loading | boolean | true while fetching or sorting. |
sorting | boolean | true while a client-side sort is in progress. |
empty | boolean | true when items is empty. |
hasMore | boolean | true when a non-null cursor is available. |
error | any | undefined | The error from the last failed fetch. |
abort() | () => void | Cancels the current in-flight request. |
reload() | () => void | Clears items and re-fetches from the beginning. |
loadMore() | () => void | Fetches the next page using the stored cursor. |
sort(descriptor) | (SortDescriptor<T>) => void | Applies a sort descriptor and triggers a sort or reload. |
setFilterText(text) | (string) => void | Updates the filter text and triggers a reload. |
clearFilter() | () => void | Clears 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