useAsyncListState
useAsyncListState manages state for an async immutable list data structure, and provides various methods to safely update the data over time. It is an extension to the useListState hook.
| Install | yarn add @diallink-corp/convergo-state-data |
|---|---|
| Version | 4.1.2 |
| Usage | import {useAsyncListState} from '@diallink-corp/convergo-state-data' |
Features
- Build immutable list data structures, that are fetched asynchronously.
- Supports pagination, sorting and filtering.
- Handles loading states and errors.
- Built-in abortable request support.
Synergy
- To render an accessible list of selectable items use a ListBox component.
- To render an accessible space-saving list of selectable items use a Select component.
Generate an Async List
To generate a new async list you need to pass in a loadItems prop to the useAsyncListState hook. This method
performs the asynchronous fetching of the data. This fetching process be performed through virtually any fetch
API, such as the browsers fetch API or Axios.
You can then use the items that are being returned by the useListState hook to render a collection of items.
function Example() {
const list = useAsyncListState({
async loadItems({ signal }) {
const response = await fetch('https://pokeapi.co/api/v2/pokemon', {
signal
});
const { results } = await response.json();
return { items: results };
}
});
return (
<Select
items={list.items}
isLoading={list.isLoading}
label="Pick your favorite Pokémon"
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Select>
);
}
function Example() {
const list = useAsyncListState({
async loadItems({ signal }) {
const response = await fetch(
'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
const { results } = await response.json();
return { items: results };
}
});
return (
<Select
items={list.items}
isLoading={list.isLoading}
label="Pick your favorite Pokémon"
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Select>
);
}
function Example() {
const list =
useAsyncListState({
async loadItems(
{ signal }
) {
const response =
await fetch(
'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
const {
results
} =
await response
.json();
return {
items: results
};
}
});
return (
<Select
items={list.items}
isLoading={list
.isLoading}
label="Pick your favorite Pokémon"
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</Select>
);
}
Infinite Scrolling
The useAsyncListState hook also supports infinite scrolling for paginated responses from APIs.
This technique is commonly used by APIs to limit the amount of data that is being returned at
a time. This can be achieved by returning a cursor value from the loadItems method. The cursor
is an indicator that points to the current position or rather page in the paginated responses.
Many of the collection-based components in Convergo support a onLoadMoreItems prop, which can be used
in combination with the infinite scrolling functionality of the useAsyncListState hook.
function Example() {
const list = useAsyncListState({
async loadItems({ cursor, signal }) {
const response = await fetch(
cursor || 'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
const { results, next } = await response.json();
return { items: results, cursor: next };
}
});
return (
<Select
items={list.items}
isLoading={list.isLoading}
onLoadMoreItems={list.loadMoreItems}
label="Pick your favorite Pokémon"
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Select>
);
}
function Example() {
const list = useAsyncListState({
async loadItems({ cursor, signal }) {
const response = await fetch(
cursor || 'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
const { results, next } = await response.json();
return { items: results, cursor: next };
}
});
return (
<Select
items={list.items}
isLoading={list.isLoading}
onLoadMoreItems={list.loadMoreItems}
label="Pick your favorite Pokémon"
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</Select>
);
}
function Example() {
const list =
useAsyncListState({
async loadItems(
{
cursor,
signal
}
) {
const response =
await fetch(
cursor ||
'https://pokeapi.co/api/v2/pokemon',
{ signal }
);
const {
results,
next
} =
await response
.json();
return {
items: results,
cursor: next
};
}
});
return (
<Select
items={list.items}
isLoading={list
.isLoading}
onLoadMoreItems={list
.loadMoreItems}
label="Pick your favorite Pokémon"
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</Select>
);
}
Reloading Items
The items of the list can easily be reloaded by calling the reloadItems method returned by the hook.
list.reloadItems();
list.reloadItems();
list.reloadItems();
Client-Side Sorting
To sort the items on the client-side, you need to pass a sortItems prop, which should be a comparator function
to sort your items.
Once the list is initiated, you can then call sortItems to change the order column or order of the items
on the fly. The hook also accepts an initialSortDescriptor prop, to configure the default sorting behaviour.
const list = useAsyncListState({
async loadItems({signal}) {
// The same loadItems method we used in the previous examples.
},
sortItems({items, sortDescriptor}) {
return {
items: items.sort((a, b) => {
let item = a[sortDescriptor.column] < b[sortDescriptor.column] ? -1 : 1;
if (sortDescriptor.direction === 'descending') {
item *= -1;
}
return item;
})
};
},
initialSortDescriptor: {column: 'name', direction: 'descending'}
});
// Call this method to change the active sorting of the list.
list.sortItems({column: 'name', direction: 'ascending'});
const list = useAsyncListState({
async loadItems({ signal }) {
// The same loadItems method we used in the previous examples.
},
sortItems({ items, sortDescriptor }) {
return {
items: items.sort((a, b) => {
let item =
a[sortDescriptor.column] <
b[sortDescriptor.column]
? -1
: 1;
if (sortDescriptor.direction === 'descending') {
item *= -1;
}
return item;
})
};
},
initialSortDescriptor: {
column: 'name',
direction: 'descending'
}
});
// Call this method to change the active sorting of the list.
list.sortItems({ column: 'name', direction: 'ascending' });
const list =
useAsyncListState({
async loadItems(
{ signal }
) {
// The same loadItems method we used in the previous examples.
},
sortItems(
{
items,
sortDescriptor
}
) {
return {
items: items
.sort(
(a, b) => {
let item =
a[
sortDescriptor
.column
] <
b[
sortDescriptor
.column
]
? -1
: 1;
if (
sortDescriptor
.direction ===
'descending'
) {
item *=
-1;
}
return item;
}
)
};
},
initialSortDescriptor:
{
column: 'name',
direction:
'descending'
}
});
// Call this method to change the active sorting of the list.
list.sortItems({
column: 'name',
direction: 'ascending'
});
Server-Side Sorting
If you want the server to perform the sorting for you, then you need to pass a sorting parameter with
your API requests. This is supported by the useAsyncListState hook via the sortDescriptor in the
loadItems method.
const list = useAsyncListState({
async loadItems({ signal, sortDescriptor }) {
const sortColumn = `direction=`;
const sortDirection = `direction=`;
const response = await fetch(
`https://api.example.com?&`,
{ signal }
);
const { results } = await response.json();
return { items: results };
}
});
const list = useAsyncListState({
async loadItems({ signal, sortDescriptor }) {
const sortColumn = `direction=`;
const sortDirection =
`direction=`;
const response = await fetch(
`https://api.example.com?&`,
{ signal }
);
const { results } = await response.json();
return { items: results };
}
});
const list =
useAsyncListState({
async loadItems(
{
signal,
sortDescriptor
}
) {
const sortColumn =
`direction=`;
const sortDirection =
`direction=`;
const response =
await fetch(
`https://api.example.com?&`,
{ signal }
);
const { results } =
await response
.json();
return {
items: results
};
}
});
Server-Side Filtering
If you want to filter the returned results on the server-side, just append the filterValue prop from
the loadItems method to your API request. You can change the filter value by calling the setFilterValue
method returned by the hook.
const list = useAsyncListState({
async loadItems({ signal, filterValue }) {
const response = await fetch(
`https://api.example.com?search=`,
{ signal }
);
const { results } = await response.json();
return { items: results };
}
});
// Call this method to change the active filter value of the list.
list.setFilterValue('New value');
const list = useAsyncListState({
async loadItems({ signal, filterValue }) {
const response = await fetch(
`https://api.example.com?search=`,
{ signal }
);
const { results } = await response.json();
return { items: results };
}
});
// Call this method to change the active filter value of the list.
list.setFilterValue('New value');
const list =
useAsyncListState({
async loadItems(
{
signal,
filterValue
}
) {
const response =
await fetch(
`https://api.example.com?search=`,
{ signal }
);
const { results } =
await response
.json();
return {
items: results
};
}
});
// Call this method to change the active filter value of the list.
list.setFilterValue(
'New value'
);
Selection Before Loading
To define which items should be selected when the list is fetched, you can pass in an initialSelectedKeys prop.
This is useful if you already know which items should be selected once the data is loaded.
const list = useAsyncListState({
async loadItems({signal}) {
// The same loadItems method we used in the previous examples.
},
initialSelectedKeys: ['John', 'Max']
});
const list = useAsyncListState({
async loadItems({ signal }) {
// The same loadItems method we used in the previous examples.
},
initialSelectedKeys: ['John', 'Max']
});
const list =
useAsyncListState({
async loadItems(
{ signal }
) {
// The same loadItems method we used in the previous examples.
},
initialSelectedKeys:
['John', 'Max']
});
Selection After Loading
To dynamically mark certain items in the list as selected based on the data returned from the server you can return
a selectedKeys value from the loadItems method.
const list = useAsyncListState({
async loadItems({ signal, filterValue }) {
const response = await fetch(
`https://api.example.com?search=`,
{ signal }
);
const { results } = await response.json();
return {
items: results,
selectedKeys: results.filter((item) => item.isSelected).map((item) =>
item.id
)
};
}
});
const list = useAsyncListState({
async loadItems({ signal, filterValue }) {
const response = await fetch(
`https://api.example.com?search=`,
{ signal }
);
const { results } = await response.json();
return {
items: results,
selectedKeys: results.filter((item) =>
item.isSelected
).map((item) => item.id)
};
}
});
const list =
useAsyncListState({
async loadItems(
{
signal,
filterValue
}
) {
const response =
await fetch(
`https://api.example.com?search=`,
{ signal }
);
const { results } =
await response
.json();
return {
items: results,
selectedKeys:
results.filter(
(item) =>
item
.isSelected
).map((item) =>
item.id
)
};
}
});
Client-Side Updates
The useAsyncListState hook is an extension to the useListState hook. The useListState hook supports many useful client-side updates such as inserting, moving and removing data from the list. The useAsyncListState hook supports all of the functionality from the useListState hook. For more details, consult the useListState docs.