useAsyncList
Manages state for an immutable async loaded list data structure, and provides convenience methods to update the data over time. Manages loading and error states, pagination, and sorting.
install | yarn add react-stately |
---|---|
version | 3.21.0 |
usage | import {useAsyncList} from 'react-stately' |
Introduction#
useAsyncList
extends on useListData, adding support for async loading, pagination, sorting, and filtering.
It manages loading and error states, supports abortable requests, and works with any data fetching library or the built-in
browser fetch API.
API#
useAsyncList<T, C = string>(
(options: AsyncListOptions<T, C>
)): AsyncListData<T>
Options#
Name | Type | Description |
load | AsyncListLoadFunction<T, C> | A function that loads the data for the items in the list. |
initialSelectedKeys | Iterable<Key> | The keys for the initially selected items. |
initialSortDescriptor | SortDescriptor | The initial sort descriptor. |
initialFilterText | string | The initial filter text. |
getKey | (
(item: T
)) => Key | A function that returns a unique key for an item object. |
sort | AsyncListLoadFunction<T, C> | An optional function that performs sorting. If not provided,
then |
Interface#
Properties
Name | Type | Description |
isLoading | boolean | Whether data is currently being loaded. |
loadingState | LoadingState | The current loading state for the list. |
items | T[] | The items in the list. |
selectedKeys | Selection | The keys of the currently selected items in the list. |
filterText | string | The current filter text. |
error | Error | If loading data failed, then this contains the error that occurred. |
sortDescriptor | SortDescriptor | The current sort descriptor for the list. |
Methods
Method | Description |
reload(): void | Reloads the data in the list. |
loadMore(): void | Loads the next page of data in the list. |
sort(
(descriptor: SortDescriptor
)): void | Triggers sorting for the list. |
setSelectedKeys(
(keys: Selection
)): void | Sets the selected keys. |
setFilterText(
(filterText: string
)): void | Sets the filter text. |
getItem(
(key: Key
)): T | Gets an item from the list by key. |
insert(
(index: number,
, ...values: T[]
)): void | Inserts items into the list at the given index. |
insertBefore(
(key: Key,
, ...values: T[]
)): void | Inserts items into the list before the item at the given key. |
insertAfter(
(key: Key,
, ...values: T[]
)): void | Inserts items into the list after the item at the given key. |
append(
(...values: T[]
)): void | Appends items to the list. |
prepend(
(...values: T[]
)): void | Prepends items to the list. |
remove(
(...keys: Key[]
)): void | Removes items from the list by their keys. |
removeSelectedItems(): void | Removes all items from the list that are currently in the set of selected items. |
move(
(key: Key,
, toIndex: number
)): void | Moves an item within the list. |
moveBefore(
(key: Key,
, keys: Iterable<Key>
)): void | Moves one or more items before a given key. |
moveAfter(
(key: Key,
, keys: Iterable<Key>
)): void | Moves one or more items after a given key. |
update(
(key: Key,
, newValue: T
)): void | Updates an item in the list. |
Example#
To construct an async list, pass a load
function to useAsyncList
that returns the items to render.
You can use the state returned by useAsyncList
to render a collection component.
This example fetches a list of Pokemon from an API and displays them in a Picker. It uses
fetch to load the data, passing through the abort signal
given by useAsyncList
and returning the results from the API. The isLoading
prop is passed to the Picker
to tell it to render the loading spinner while data is loading.
let list = useAsyncList({
async load({signal}) {
let res = await fetch('https://pokeapi.co/api/v2/pokemon', {signal});
let json = await res.json();
return {items: json.results};
}
});
<Picker
label="Pick a Pokemon"
items={list.items}
isLoading={list.isLoading}>
{item => <Item key={item.name}>{item.name}</Item>}
</Picker>
let list = useAsyncList({
async load({ signal }) {
let res = await fetch(
'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
let json = await res.json();
return { items: json.results };
}
});
<Picker
label="Pick a Pokemon"
items={list.items}
isLoading={list.isLoading}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Picker>
let list = useAsyncList({
async load(
{ signal }
) {
let res =
await fetch(
'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
let json = await res
.json();
return {
items: json.results
};
}
});
<Picker
label="Pick a Pokemon"
items={list.items}
isLoading={list
.isLoading}
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</Picker>
Infinite loading#
useAsyncList
also supports paginated data, which is common in many APIs to avoid loading too many items at once.
This is accomplished by returning a cursor in addition to items
from the load function. When the loadMore
method
is called, the cursor is passed back to your load
function, which you can use to determine the URL for the next
page. The onLoadMore
prop supported by many collection components notifies you when you should load more data
as the user scrolls.
This example expands on the previous one to support infinite scrolling through all known Pokemon. It returns the
next
property from the API response as the cursor
, and uses it instead of the original API URL for subsequent
page loads. It passes the onLoadMore
prop to Picker, which triggers loading more items as the user scrolls down.
let list = useAsyncList({
async load({ signal, cursor }) {
// If no cursor is available, then we're loading the first page.
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
let res = await fetch(cursor || 'https://pokeapi.co/api/v2/pokemon', {
signal
});
let json = await res.json();
return {
items: json.results,
cursor: json.next
};
}
});
<Picker
label="Pick a Pokemon"
items={list.items}
isLoading={list.isLoading}
onLoadMore={list.loadMore}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Picker>
let list = useAsyncList({
async load({ signal, cursor }) {
// If no cursor is available, then we're loading the first page.
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
let res = await fetch(
cursor || 'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
let json = await res.json();
return {
items: json.results,
cursor: json.next
};
}
});
<Picker
label="Pick a Pokemon"
items={list.items}
isLoading={list.isLoading}
onLoadMore={list.loadMore}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Picker>
let list = useAsyncList({
async load(
{ signal, cursor }
) {
// If no cursor is available, then we're loading the first page.
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
let res =
await fetch(
cursor ||
'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
let json = await res
.json();
return {
items:
json.results,
cursor: json.next
};
}
});
<Picker
label="Pick a Pokemon"
items={list.items}
isLoading={list
.isLoading}
onLoadMore={list
.loadMore}
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</Picker>
Reloading data#
Data can be reloaded by calling the reload
method of the list.
list.reload();
list.reload();
list.reload();
Sorting#
Some components like tables support sorting data. You may also have custom UI to implement this in other components.
This can be implemented by passing a sort
function to useAsyncList
, or by using the sortDescriptor
passed to
load
if no sort
function is given. Passing a separate sort
function could be useful when implementing client side
sorting. Using the sortDescriptor
in load
is useful when you need to implement server side sorting, which might be
an API parameter.
Client side sorting#
This example shows how to implement client side sorting by passing a sort
function to useAsyncList
and sorting the
items array.
let collator = useCollator();
let list = useAsyncList({
async load({ signal }) {
// Same load function as before
},
sort({ items, sortDescriptor }) {
return {
items: items.sort((a, b) => {
// Compare the items by the sorted column
let cmp = collator.compare(
a[sortDescriptor.column],
b[sortDescriptor.column]
);
// Flip the direction if descending order is specified.
if (sortDescriptor.direction === 'descending') {
cmp *= -1;
}
return cmp;
})
};
}
});
let collator = useCollator();
let list = useAsyncList({
async load({ signal }) {
// Same load function as before
},
sort({ items, sortDescriptor }) {
return {
items: items.sort((a, b) => {
// Compare the items by the sorted column
let cmp = collator.compare(
a[sortDescriptor.column],
b[sortDescriptor.column]
);
// Flip the direction if descending order is specified.
if (sortDescriptor.direction === 'descending') {
cmp *= -1;
}
return cmp;
})
};
}
});
let collator =
useCollator();
let list = useAsyncList({
async load(
{ signal }
) {
// Same load function as before
},
sort(
{
items,
sortDescriptor
}
) {
return {
items: items.sort(
(a, b) => {
// Compare the items by the sorted column
let cmp =
collator
.compare(
a[
sortDescriptor
.column
],
b[
sortDescriptor
.column
]
);
// Flip the direction if descending order is specified.
if (
sortDescriptor
.direction ===
'descending'
) {
cmp *= -1;
}
return cmp;
}
)
};
}
});
Server side sorting#
Server side sorting could be implemented by using the sortDescriptor
in the load
function, and passing a
parameter to the API.
let list = useAsyncList({
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
};
}
});
let list = useAsyncList({
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
};
}
});
let list = useAsyncList({
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#
There are many instances where your list of data may need to be filtered, such as during user lookup or query searches.
For server side filtering, this can be implemented by using the filterText
option passed to the load
function.
The setFilterText
method updates the current filterText
value and triggers the load
function. This allows
you to reload the results with the new filter text.
Server side filtering#
The example below shows how server side filtering could be implemented by using filterText
in the load
function and passing a parameter to the API.
The input value of the ComboBox is controlled by providing list.filterText
as the ComboBox's inputValue
prop, and list.setFilterText
as the onInputChange
prop.
The loadingState
prop is also used to show the appropriate loading indicator depending on the state of the list.
let list = useAsyncList({
async load({ signal, filterText }) {
let res = await fetch(
`https://swapi.py4e.com/api/people/?search= `,
{ signal }
);
let json = await res.json();
return {
items: json.results
};
}
});
<ComboBox
label="Star Wars Character Lookup"
items={list.items}
inputValue={list.filterText}
onInputChange={list.setFilterText}
loadingState={list.loadingState}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</ComboBox>
let list = useAsyncList({
async load({ signal, filterText }) {
let res = await fetch(
`https://swapi.py4e.com/api/people/?search= `,
{ signal }
);
let json = await res.json();
return {
items: json.results
};
}
});
<ComboBox
label="Star Wars Character Lookup"
items={list.items}
inputValue={list.filterText}
onInputChange={list.setFilterText}
loadingState={list.loadingState}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</ComboBox>
let list = useAsyncList({
async load(
{
signal,
filterText
}
) {
let res =
await fetch(
`https://swapi.py4e.com/api/people/?search= `,
{ signal }
);
let json = await res
.json();
return {
items: json.results
};
}
});
<ComboBox
label="Star Wars Character Lookup"
items={list.items}
inputValue={list
.filterText}
onInputChange={list
.setFilterText}
loadingState={list
.loadingState}
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</ComboBox>
Pre-selecting items#
useAsyncList
manages selection state for the list in addition to its data. If you need to programmatically select items
during the initial load, you can do so using the initialSelectedKeys
option or by returning selectedKeys
from the
load
function in addition to items
.
Selecting before loading#
If you know what keys to select before items are loaded from the server, use the initialSelectedKeys
option.
let list = useAsyncList({
initialSelectedKeys: ['foo', 'bar'],
async load({signal}) {
// Same load function as before
}
});
let list = useAsyncList({
initialSelectedKeys: ['foo', 'bar'],
async load({signal}) {
// Same load function as before
}
});
let list = useAsyncList({
initialSelectedKeys: [
'foo',
'bar'
],
async load(
{ signal }
) {
// Same load function as before
}
});
Selecting based on loaded data#
If you need to compute which keys to select based on the loaded data, return selectedKeys
from the load
function
in addition to the items
.
let list = useAsyncList({
async load({ signal }) {
let res = await fetch('http://example.com/api', { signal });
let json = await res.json();
// Return items and compute selectedKeys based on the data and return a list of ids.
return {
items: json.results,
selectedKeys: json.results.filter((item) => item.isSelected).map((item) =>
item.id
)
};
}
});
let list = useAsyncList({
async load({ signal }) {
let res = await fetch('http://example.com/api', {
signal
});
let json = await res.json();
// Return items and compute selectedKeys based on the data and return a list of ids.
return {
items: json.results,
selectedKeys: json.results.filter((item) =>
item.isSelected
).map((item) => item.id)
};
}
});
let list = useAsyncList({
async load(
{ signal }
) {
let res =
await fetch(
'http://example.com/api',
{ signal }
);
let json = await res
.json();
// Return items and compute selectedKeys based on the data and return a list of ids.
return {
items:
json.results,
selectedKeys: json
.results.filter(
(item) =>
item
.isSelected
).map((item) =>
item.id
)
};
}
});
Client side updates#
All client side updating methods supported by useListData
are also supported by useAsyncList
.
See the docs for useListData for more details.