Async List
The async list is used to display a list of items that are loaded asynchronously. Usually paired with the combobox or select component.
This was inspired by React Stately's useAsyncList hook.
Features
- Support for pagination, sorting, and filtering
- Support for abortable requests via
AbortSignal
- Manages loading and error states
Installation
To use the Async List machine in your project, run the following command in your command line:
npm install @zag-js/async-list @zag-js/react # or yarn add @zag-js/async-list @zag-js/react
npm install @zag-js/async-list @zag-js/solid # or yarn add @zag-js/async-list @zag-js/solid
npm install @zag-js/async-list @zag-js/vue # or yarn add @zag-js/async-list @zag-js/vue
npm install @zag-js/async-list @zag-js/svelte # or yarn add @zag-js/async-list @zag-js/svelte
Usage
First, import the async list package into your project
import * as asyncList from "@zag-js/async-list"
The async list package exports two key functions:
machine
— The state machine logic for the async list widget.connect
— returns the properties and methods for the async data management.
import * as asyncList from "@zag-js/async-list" import { useMachine, normalizeProps } from "@zag-js/react" function AsyncList() { const service = useMachine(asyncList.machine, { async load({ signal }) { const res = await fetch(`/api/items`, { signal }) const json = await res.json() return { items: json.results, cursor: json.next, } }, }) const api = asyncList.connect(service, normalizeProps) return ( <div> <div> <pre>{JSON.stringify(api.items, null, 2)}</pre> <input type="text" onChange={(e) => api.setFilterText(e.target.value)} /> </div> <div> {api.loading && <p>Loading...</p>} <button onClick={() => api.reload()}>Reload</button> <button onClick={() => api.loadMore()}>Load More</button> <button onClick={() => api.sort({ column: "name", direction: "ascending" })} > Sort by name </button> </div> </div> ) }
import * as asyncList from "@zag-js/async-list" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, Show } from "solid-js" function AsyncList() { const service = useMachine(asyncList.machine, { async load({ signal }) { const res = await fetch(`/api/items`, { signal }) const json = await res.json() return { items: json.results, cursor: json.next, } }, }) const api = createMemo(() => asyncList.connect(service, normalizeProps)) return ( <div> <div> <pre>{JSON.stringify(api().items, null, 2)}</pre> <input type="text" onInput={(e) => api().setFilterText(e.target.value)} /> </div> <div> <Show when={api().loading}> <p>Loading...</p> </Show> <button onClick={() => api().reload()}>Reload</button> <button onClick={() => api().loadMore()}>Load More</button> <button onClick={() => api().sort({ column: "name", direction: "ascending" })} > Sort by name </button> </div> </div> ) }
<script setup> import * as asyncList from "@zag-js/async-list" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(asyncList.machine, { async load({ signal }) { const res = await fetch(`/api/items?id=${id}`, { signal }) const json = await res.json() return { items: json.results, cursor: json.next, } }, }) const api = computed(() => asyncList.connect(service.value, normalizeProps)) </script> <template> <div> <div> <pre>{{ JSON.stringify(api.items, null, 2) }}</pre> <input type="text" @input="(e) => api.setFilterText(e.target.value)" /> </div> <div> <p v-if="api.loading">Loading...</p> <button @click="() => api.reload()">Reload</button> <button @click="() => api.loadMore()">Load More</button> <button @click="() => api.sort({ column: 'name', direction: 'ascending' })" > Sort by name </button> </div> </div> </template>
<script> import * as asyncList from "@zag-js/async-list" import { useMachine, normalizeProps } from "@zag-js/svelte" const service = useMachine(asyncList.machine, { async load({ signal }) { const res = await fetch(`/api/items`, { signal }) const json = await res.json() return { items: json.results, cursor: json.next, } }, }) const api = $derived(asyncList.connect(service, normalizeProps)) </script> <div> <div> <pre>{JSON.stringify(api.items, null, 2)}</pre> <input type="text" oninput={(e) => api.setFilterText(e.target.value)} /> </div> <div> {#if api.loading} <p>Loading...</p> {/if} <button onclick={() => api.reload()}>Reload</button> <button onclick={() => api.loadMore()}>Load More</button> <button onclick={() => api.sort({ column: "name", direction: "ascending" })} > Sort by name </button> </div> </div>
Loading and Error States
The async list machine will automatically return the error
and loading
properties in the api
when the load
function returns an error or is loading.
api.error
— The error instance returned by the last fetch.api.loading
— Whether the list is loading.api.items
— The items in the list after the last fetch.
Pagination
The async list supports paginated data to avoid loading too many items at once.
This is accomplished by returning a cursor in addition to items from the load
function.
When loadMore
is called, the cursor is passed back to your load
function,
which you can use to determine the URL for the next page.
const service = useMachine(asyncList.machine, { async load({ signal, cursor }) { const requestUrl = cursor || "https://pokeapi.co/api/v2/pokemon" const res = await fetch(requestUrl, { signal }) const json = await res.json() return { items: json.results, cursor: json.next, } }, })
Then, you can use the api.loadMore
method to load more items.
api.loadMore()
Reloading the data
Use the api.reload
method to reload the data.
api.reload()
Sorting
The async list machine supports both client-side and server-side sorting. You
can implement sorting by providing a sort
function for client-side operations,
or by handling the sortDescriptor
parameter in your load
function for
server-side sorting.
Regardless of the sorting implementation, the way to trigger the sorting is by
calling the api.sort
method with the descriptor.
api.sort({ column: "name", direction: "ascending" })
Client-side sorting
Use the sort
function to implement client-side sorting.
const service = useMachine(asyncList.machine, { async load({ signal }) { // ... }, sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { // Compare the items by the sorted column const aColumn = a[sortDescriptor.column] const bColumn = b[sortDescriptor.column] let direction = aColumn.localeCompare(bColumn) // Flip the direction if descending order is specified. if (sortDescriptor.direction === "descending") { direction *= -1 } return direction }), } }, })
Server-side sorting
For server-side sorting, use the sortDescriptor
parameter in the load
function to pass sorting parameters to your API.
const service = useMachine(asyncList.machine, { async load({ signal, sortDescriptor }) { let url = new URL("http://example.com/api") if (sortDescriptor) { url.searchParams.append("sort_key", sortDescriptor.column) url.searchParams.append("sort_direction", sortDescriptor.direction) } let res = await fetch(url, { signal }) let json = await res.json() return { items: json.results, } }, })
Filtering
Filtering your data list is often necessary, such as for user searches or query
lookups. For server-side filtering, use the filterText
parameter in the load
function.
The way to trigger the filtering is by calling the api.setFilterText
method
with the filter text.
api.setFilterText("filter text")
The api.setFilterText
method modifies the filterText
and calls the load
function to refresh the data with the updated filter.
const service = useMachine(asyncList.machine, { async load({ signal, filterText }) { let url = new URL("http://example.com/api") if (filterText) { url.searchParams.append("filter", filterText) } let res = await fetch(url, { signal }) let json = await res.json() return { items: json.results, } }, })
Aborting requests
Use the api.abort
method to abort the current request.
api.abort()
This only works if you pass the signal
parameter to the load
function.
const service = useMachine(asyncList.machine, { async load({ signal }) { // ... }, })
Registering external dependencies
In even more complex scenarios, you may need to register external dependencies that trigger a reload of the data.
Use the dependencies
parameter to register external dependencies. Ensure every
dependency is a primitive value (no objects, arrays, maps, sets, etc.).
const service = useMachine(asyncList.machine, { dependencies: ["user"], async load({ signal, deps }) { // ... }, })
Methods and Properties
The async list's api
exposes the following methods and properties:
Machine Context
The async list machine exposes the following context properties:
load
(args: LoadDetails<C>) => Promise<LoadResult<T, C>>
The function to call when the list is loadedsort
(args: SortDetails<T>) => Promise<{ items: T[]; }> | { items: T[]; }
The function to call when the list is sortedinitialItems
T[]
The initial items to displayinitialSortDescriptor
SortDescriptor
The initial sort descriptor to useinitialFilterText
string
The initial filter text to usedependencies
LoadDependency[]
The dependencies to watch for changesautoReload
boolean
Whether to automatically reload the list when the dependencies changeonSuccess
(details: { items: T[]; }) => void
The function to call when the list is loaded successfullyonError
(details: { error: Error; }) => void
The function to call when the list fails to load
Machine API
The async list api
exposes the following methods:
items
T[]
The items in the list.filterText
string
The filter text.cursor
C
The cursor.sortDescriptor
SortDescriptor
The sort descriptor.loading
boolean
Whether the list is loading.error
any
The error instance returned by the last fetch.abort
() => void
Function to abort the current fetch.reload
() => void
Function to reload the listloadMore
() => void
Function to load more itemssort
(sortDescriptor: SortDescriptor) => void
Function to sort the listsetFilterText
(filterText: string) => void
Function to set the filter text
Edit this page on GitHub