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.

Installyarn add @diallink-corp/convergo-state-data
Version4.1.2
Usageimport {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=${sortDescriptor.column}`;
    const sortDirection = `direction=${sortDescriptor.direction}`;
    const response = await fetch(
      `https://api.example.com?${sortColumn}&${sortDirection}`,
      { signal }
    );
    const { results } = await response.json();
    return { items: results };
  }
});
const list = useAsyncListState({
  async loadItems({ signal, sortDescriptor }) {
    const sortColumn = `direction=${sortDescriptor.column}`;
    const sortDirection =
      `direction=${sortDescriptor.direction}`;
    const response = await fetch(
      `https://api.example.com?${sortColumn}&${sortDirection}`,
      { signal }
    );
    const { results } = await response.json();
    return { items: results };
  }
});
const list =
  useAsyncListState({
    async loadItems(
      {
        signal,
        sortDescriptor
      }
    ) {
      const sortColumn =
        `direction=${sortDescriptor.column}`;
      const sortDirection =
        `direction=${sortDescriptor.direction}`;
      const response =
        await fetch(
          `https://api.example.com?${sortColumn}&${sortDirection}`,
          { 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=${filterValue}`,
      { 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=${filterValue}`,
      { 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=${filterValue}`,
          { 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=${filterValue}`,
      { 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=${filterValue}`,
      { 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=${filterValue}`,
          { 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.

API