Skip to main content

Loads, filters and pages a list of items asynchronously, without any DOM. A thin wrapper around the machine of @zag-js/async-list.

AsyncList has no rendering of its own: you create it with a load function, subscribe to it and render the items yourself. The Combobox Async Search example uses it to fill the list from an Extbase action while the user types.

import { debounce } from '@zag-js/utils';
import { AsyncList, extbase } from 'fluid-primitives';

interface City {
    value: string;
    title: string;
}

const list = new AsyncList<City>({
    load: async ({ filter, signal }) => {
        const response = await extbase.post(searchUrl, { q: filter }, { signal });
        return { items: (await response.json()) as City[] };
    },
});

list.subscribe(api => {
    renderItems(api.items);
    spinnerEl.hidden = !api.isLoading;
});

const setFilter = debounce((filter: string) => list.setFilter(filter), 300);
inputEl.addEventListener('input', () => setFilter(inputEl.value));

list.init();

setFilter() reloads the list straight away. Debouncing it is up to you, so you decide the delay and can skip it where it isn't needed, e.g. when the list only reloads from dependencies. Call list.destroy() when you are done with it.

The options of an AsyncList: the props of the @zag-js/async-list machine.

NameDescriptionRequiredDefault
load(args: LoadDetails<Item, Filter, Sorting, Cursor>) => Promise<LoadResult<Item, Cursor>>
The function to call when the list is loaded
Yes
-
sort((args: SortDetails<Item, Filter, Sorting>) => Promise<{ items: Item[]; }> | { items: Item[]; } | undefined) | undefined
The function to call when the list is sorted client-side. When provided, sorting is performed locally instead of triggering a new load.
No
-
initialItemsItem[] | undefined
The initial items to display
No
-
initialSortingSorting | undefined
The initial sorting state
No
-
initialFilterFilter | undefined
The initial filter state
No
-
keepPreviousItemsboolean | undefined
Whether to keep the previous items visible while new data is loading. Prevents content flash when filter or sort changes.
No
false
dependenciesLoadDependency[] | undefined
The dependencies to watch for changes
No
-
autoReloadboolean | undefined
Whether to automatically reload the list when the dependencies change
No
-
onSuccess((details: { items: Item[]; }) => void | undefined) | undefined
The function to call when the list is loaded successfully
No
-
onError((details: { error: Error; }) => void | undefined) | undefined
The function to call when the list fails to load
No
-

Wraps the machine of @zag-js/async-list for use without any DOM or hydration: construct it, call init(), subscribe() to state changes and destroy() when done. Rendering is yours, do it from inside subscribe().

Debouncing or throttling setFilter() is left to the caller, wrap the call site with the debounce or throttle of @zag-js/utils, which this library already depends on.

class AsyncList<T, Filter = string, Sorting = asyncList.SortDescriptor<T>, Cursor = string>
NameDescriptionRequiredDefault
optionsAsyncListOptions<T, Filter, Sorting, Cursor>Yes
-
NameDescription
apiasyncList.Api<T, Filter, Sorting, Cursor>
The api of the current state of the list. It is read fresh on every access, don't hold on to it.

subscribe()

Calls fn with the api on every future state change. It does not fire for the current state, read api first if you need it.

subscribe(fn: (api: asyncList.Api<T, Filter, Sorting, Cursor>) => void): () => void
NameDescriptionRequiredDefault
fn(api: asyncList.Api<T, Filter, Sorting, Cursor>) => void
The listener.
Yes
-

Returns () => void. A function that removes the listener.

setFilter()

Sets the filter and reloads the list with it.

setFilter(filter: Filter): void
NameDescriptionRequiredDefault
filterFilterYes
-

updateProps()

Changes options at runtime, e.g. dependencies to trigger an autoReload. load is fixed when the list is constructed and can't be swapped here.

updateProps(newProps: Partial<Omit<asyncList.Props<T, Filter, Sorting, Cursor>, 'load'>>): void
NameDescriptionRequiredDefault
newPropsPartial<Omit<asyncList.Props<T, Filter, Sorting, Cursor>, 'load'>>Yes
-

init()

Starts the machine, which loads the list for the first time. Call it once, after constructing.

init(): void

destroy()

Stops the machine and cancels a load that is in flight.

destroy(): void

list.api and the argument of a subscribe() listener are the api of the machine:

NameTypeDescription
itemsItem[]The items in the list.
filterFilterThe current filter state.
cursorCursor | undefinedThe cursor for pagination.
sortingSorting | undefinedThe current sorting state.
isLoadingbooleanWhether the list is loading (includes both fetching and sorting).
isLoadingMorebooleanWhether the list is loading more items via pagination.
isSortingbooleanWhether a client-side sort is in progress.
isEmptybooleanWhether the list is empty.
hasMorebooleanWhether there are more items to load.
errorError | undefinedThe error instance returned by the last fetch.
abortVoidFunctionFunction to abort the current fetch.
reloadVoidFunctionFunction to reload the list
loadMoreVoidFunctionFunction to load more items
setSorting(sorting: Updater<Sorting | undefined>) => voidFunction to update the sorting state and trigger a sort. Accepts a value or an updater function.
setFilter(filter: Updater<Filter>) => voidFunction to update the filter state and trigger a reload. Accepts a value or an updater function.
clearFilterVoidFunctionFunction to reset the filter to `initialFilter`. No-op if `initialFilter` was not provided.