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 sortedinitialItemsT[]The initial items to displayinitialSortDescriptorSortDescriptorThe initial sort descriptor to useinitialFilterTextstringThe initial filter text to usedependenciesLoadDependency[]The dependencies to watch for changesautoReloadbooleanWhether to automatically reload the list when the dependencies changeonSuccess(details: { items: T[]; }) => voidThe function to call when the list is loaded successfullyonError(details: { error: Error; }) => voidThe function to call when the list fails to load
Machine API
The async list api exposes the following methods:
itemsT[]The items in the list.filterTextstringThe filter text.cursorCThe cursor.sortDescriptorSortDescriptorThe sort descriptor.loadingbooleanWhether the list is loading.erroranyThe error instance returned by the last fetch.abortVoidFunctionFunction to abort the current fetch.reloadVoidFunctionFunction to reload the listloadMoreVoidFunctionFunction to load more itemssort(sortDescriptor: SortDescriptor) => voidFunction to sort the listsetFilterText(filterText: string) => voidFunction to set the filter text
Edit this page on GitHub