# Async List

URL: https://ark-ui.com/docs/collections/async-list
Source: https://raw.githubusercontent.com/chakra-ui/ark/refs/heads/main/website/src/content/pages/collections/async-list.mdx

A hook for managing asynchronous data operations in list collections including loading, filtering, sorting, and pagination.

---

The `useAsyncList` hook manages asynchronous data operations for list collections. It provides a comprehensive solution
for loading, filtering, sorting, and paginating data with built-in loading states, error handling, and request
cancellation.

```tsx
import { useAsyncList } from 'ark-ripple/collection'

const list = useAsyncList<User>({
  async load({ signal }) {
    const response = await fetch('/api/users', { signal })
    const users = await response.json()
    return { items: users }
  },
})

console.log(list.items) // User[]
console.log(list.loading) // boolean
console.log(list.error) // Error | null
```

## Examples

### Loading Data

Load data asynchronously from an API using the `load` function and update the list when the data is ready.

**Example: async-list/reload**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { Loader } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/async-list.module.css';

interface Quote {
  id: number;
  quote: string;
  author: string;
}

export component Reload() {
  const list = useAsyncList<Quote>(
    {
      autoReload: true,
      async load() {
        const response = await fetch(
          `https://dummyjson.com/quotes?limit=4&skip=${Math.floor(Math.random() * 50)}`,
        );

        if (!response.ok) throw new Error('Failed to fetch quotes');

        const data = await response.json();
        return { items: data.quotes };
      },
    },
  );

  <div class={styles.Root}>
    <div class={styles.Header}>
      <button
        class={button.Root}
        onClick={() => {
          @list.reload();
        }}
        disabled={@list.loading}
      >
        if (@list.loading) {
          <Loader class={styles.Spinner} />
          {'Loading'}
        } else {
          {'Reload Quotes'}
        }
      </button>
    </div>

    if (@list.error) {
      <div class={styles.Error}>
        {'Error: '}
        {@list.error.message}
      </div>
    }

    <div class={styles.ItemGroup}>
      for (const quote of @list.items; key quote.id) {
        <div class={styles.Item}>
          <div class={styles.ItemContent}>
            <div class={styles.ItemDescription}>
              {'"'}
              {quote.quote}
              {'"'}
            </div>
            <div class={styles.ItemMeta}>
              {'— '}
              {quote.author}
            </div>
          </div>
        </div>
      }
    </div>
  </div>
}

```

### Infinite Loading

Implement pagination by returning a cursor that indicates more data is available.

**Example: async-list/infinite-loading**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { Loader } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/async-list.module.css';

const LIMIT = 4;

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

export component InfiniteLoading() {
  const list = useAsyncList<Post, number>(
    {
      autoReload: true,
      async load({ cursor }: { cursor?: number }) {
        const page = cursor || 1;
        const start = (page - 1) * LIMIT;

        const response = await fetch(
          `https://jsonplaceholder.typicode.com/posts?_start=${start}&_limit=${LIMIT}`,
        );

        if (!response.ok) throw new Error('Failed to fetch posts');

        const posts: Post[] = await response.json();
        const hasNextPage = posts.length === LIMIT;

        return {
          items: posts,
          cursor: hasNextPage ? page + 1 : undefined,
        };
      },
    },
  );

  <div class={styles.Root}>
    <div class={styles.Header}>
      <span class={styles.Status}>
        {'Loaded '}
        {@list.items.length}
        {' posts'}
        {@list.cursor ? ' (more available)' : null}
      </span>
      if (@list.cursor) {
        <button
          class={button.Root}
          onClick={() => {
            @list.loadMore();
          }}
          disabled={@list.loading}
        >
          if (@list.loading) {
            <Loader class={styles.Spinner} />
            {'Loading'}
          } else {
            {'Load More'}
          }
        </button>
      }
    </div>

    if (@list.error) {
      <div class={styles.Error}>
        {'Error: '}
        {@list.error.message}
      </div>
    }

    <div class={styles.ItemGroup}>
      for (const post of @list.items; key post.id) {
        <div class={styles.Item}>
          <div class={styles.ItemContent}>
            <div class={styles.ItemTitle}>
              {@list.items.indexOf(post) + 1}
              {'. '}
              {post.title}
            </div>
            <div class={styles.ItemDescription}>{post.body}</div>
          </div>
        </div>
      }
    </div>
  </div>
}

```

### Filtering

Filter data based on user input with automatic debouncing and loading states.

**Example: async-list/filter**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { Loader } from 'lucide-ripple';
import field from 'styles/field.module.css';
import styles from 'styles/async-list.module.css';

const LIMIT = 4;

interface User {
  id: number;
  name: string;
  email: string;
  department: string;
  role: string;
}

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const mockUsers: User[] = [
  {
    id: 1,
    name: 'Alice Johnson',
    email: 'alice@example.com',
    department: 'Engineering',
    role: 'Senior Developer',
  },
  {
    id: 2,
    name: 'Bob Smith',
    email: 'bob@example.com',
    department: 'Marketing',
    role: 'Marketing Manager',
  },
  {
    id: 3,
    name: 'Carol Davis',
    email: 'carol@example.com',
    department: 'Engineering',
    role: 'Frontend Developer',
  },
  {
    id: 4,
    name: 'David Wilson',
    email: 'david@example.com',
    department: 'Sales',
    role: 'Sales Representative',
  },
  {
    id: 5,
    name: 'Eva Brown',
    email: 'eva@example.com',
    department: 'Engineering',
    role: 'DevOps Engineer',
  },
  {
    id: 6,
    name: 'Frank Miller',
    email: 'frank@example.com',
    department: 'Support',
    role: 'Customer Success',
  },
  {
    id: 7,
    name: 'Grace Lee',
    email: 'grace@example.com',
    department: 'Marketing',
    role: 'Content Creator',
  },
  {
    id: 8,
    name: 'Henry Taylor',
    email: 'henry@example.com',
    department: 'Engineering',
    role: 'Backend Developer',
  },
  {
    id: 9,
    name: 'Ivy Anderson',
    email: 'ivy@example.com',
    department: 'Sales',
    role: 'Account Manager',
  },
  {
    id: 10,
    name: 'Jack Thompson',
    email: 'jack@example.com',
    department: 'Support',
    role: 'Technical Support',
  },
  {
    id: 11,
    name: 'Kate Martinez',
    email: 'kate@example.com',
    department: 'Marketing',
    role: 'Brand Manager',
  },
  {
    id: 12,
    name: 'Liam Garcia',
    email: 'liam@example.com',
    department: 'Engineering',
    role: 'Full Stack Developer',
  },
  {
    id: 13,
    name: 'Mia Rodriguez',
    email: 'mia@example.com',
    department: 'Sales',
    role: 'Sales Director',
  },
  {
    id: 14,
    name: 'Noah Lopez',
    email: 'noah@example.com',
    department: 'Support',
    role: 'Support Manager',
  },
  {
    id: 15,
    name: 'Olivia White',
    email: 'olivia@example.com',
    department: 'Engineering',
    role: 'UI Designer',
  },
  {
    id: 16,
    name: 'Paul Harris',
    email: 'paul@example.com',
    department: 'Marketing',
    role: 'Digital Marketer',
  },
  {
    id: 17,
    name: 'Quinn Clark',
    email: 'quinn@example.com',
    department: 'Engineering',
    role: 'Mobile Developer',
  },
  {
    id: 18,
    name: 'Ruby Lewis',
    email: 'ruby@example.com',
    department: 'Sales',
    role: 'Business Development',
  },
  {
    id: 19,
    name: 'Sam Young',
    email: 'sam@example.com',
    department: 'Support',
    role: 'Documentation Specialist',
  },
  {
    id: 20,
    name: 'Tina Walker',
    email: 'tina@example.com',
    department: 'Marketing',
    role: 'Social Media Manager',
  },
];

export component Filter() {
  const list = useAsyncList<User>(
    {
      initialItems: mockUsers.slice(0, LIMIT),
      async load({ filterText }: { filterText?: string }) {
        await delay(500);
        if (!filterText) return { items: mockUsers.slice(0, LIMIT) };
        const filtered = mockUsers.filter(
          (user) => user.name.toLowerCase().includes(filterText.toLowerCase()) ||
            user.email.toLowerCase().includes(filterText.toLowerCase()),
        );
        return { items: filtered.slice(0, LIMIT) };
      },
    },
  );

  <div class={styles.Root}>
    <div class={styles.Header}>
      <input
        class={field.Input}
        type="text"
        placeholder="Search users..."
        value={@list.filterText}
        onInput={(e) => {
          @list.setFilterText((e.target as HTMLInputElement).value);
        }}
      />
      if (@list.loading) {
        <span class={styles.Loading}>
          <Loader class={styles.Spinner} />
          {'Searching'}
        </span>
      }
    </div>

    if (@list.error) {
      <div class={styles.Error}>
        {'Error: '}
        {@list.error.message}
      </div>
    }

    <div class={styles.ItemGroup}>
      for (const user of @list.items; key user.id) {
        <div class={styles.Item}>
          <div class={styles.ItemContent}>
            <div class={styles.ItemTitle}>{user.name}</div>
            <div class={styles.ItemDescription}>{user.email}</div>
            <div class={styles.ItemMeta}>
              {user.department}
              {' • '}
              {user.role}
            </div>
          </div>
        </div>
      }
    </div>

    if (@list.items.length === 0 && !@list.loading) {
      <div class={styles.Empty}>{'No results found'}</div>
    }
  </div>
}

```

### Sorting (Client-side)

Sort data on the client side after loading from the server.

**Example: async-list/sort-client-side**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { ArrowDown, ArrowUpDown, ArrowUp, Loader } from 'lucide-ripple';
import styles from 'styles/async-list.module.css';

interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  phone: string;
  website: string;
}

const collator = new Intl.Collator();

const cols: { key: keyof User; label: string }[] = [
  { key: 'name', label: 'Name' },
  { key: 'username', label: 'Username' },
  { key: 'email', label: 'Email' },
];

export component SortClientSide() {
  const list = useAsyncList<User>(
    {
      autoReload: true,
      load: async () => {
        const response = await fetch('https://jsonplaceholder.typicode.com/users?_limit=5');
        const data = await response.json();
        return { items: data };
      },
      sort({ items, descriptor }) {
        return {
          items: items.sort((a, b) => {
            const { column, direction } = descriptor;
            let cmp = collator.compare(
              String(a[column as keyof User]),
              String(b[column as keyof User]),
            );
            if (direction === 'descending') {
              cmp *= -1;
            }
            return cmp;
          }),
        };
      },
    },
  );

  const handleSort = (column: keyof User) => {
    const currentSort = @list.sortDescriptor;
    let direction: 'ascending' | 'descending' = 'ascending';
    if (currentSort?.column === column && currentSort.direction === 'ascending') {
      direction = 'descending';
    }
    @list.sort({ column, direction });
  };

  <div class={styles.Root}>
    if (@list.loading) {
      <div class={styles.Loading}>
        <Loader class={styles.Spinner} />
        {'Loading'}
      </div>
    }
    if (@list.error) {
      <div class={styles.Error}>
        {'Error: '}
        {@list.error.message}
      </div>
    }
    <div class={styles.Status}>
      {'Sorted by: '}
      {@list.sortDescriptor
        ? `${@list.sortDescriptor.column} (${@list.sortDescriptor.direction})`
        : 'none'}
    </div>

    <table class={styles.Table}>
      <thead>
        <tr>
          for (const col of cols; key col.key) {
            <th onClick={() => handleSort(col.key)}>
              {col.label}
              {' '}
              if (!@list.sortDescriptor || @list.sortDescriptor.column !== col.key) {
                <ArrowUpDown />
              } else if (@list.sortDescriptor.direction === 'ascending') {
                <ArrowUp />
              } else {
                <ArrowDown />
              }
            </th>
          }
        </tr>
      </thead>
      <tbody>
        for (const user of @list.items; key user.id) {
          <tr>
            <td>{user.name}</td>
            <td>{user.username}</td>
            <td>{user.email}</td>
          </tr>
        }
      </tbody>
    </table>
  </div>
}

```

### Sorting (Server-side)

Send sort parameters to the server and reload data when sorting changes.

**Example: async-list/sort-server-side**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { ArrowDown, ArrowUpDown, ArrowUp, Loader } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/async-list.module.css';

interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  image: string;
  rating: {
    rate: number;
    count: number;
  };
}

export component SortServerSide() {
  const list = useAsyncList<Product>(
    {
      autoReload: true,
      async load({ sortDescriptor }: { sortDescriptor?: { column: string; direction: string } }) {
        const url = new URL('https://fakestoreapi.com/products');
        url.searchParams.set('limit', '5');
        if (sortDescriptor) {
          const { direction } = sortDescriptor;
          url.searchParams.set('sort', direction === 'ascending' ? 'asc' : 'desc');
        }
        const response = await fetch(url);
        const items = await response.json();
        return { items };
      },
    },
  );

  const handleSort = (column: keyof Product) => {
    const currentSort = @list.sortDescriptor;
    let direction: 'ascending' | 'descending' = 'ascending';
    if (currentSort?.column === column && currentSort.direction === 'ascending') {
      direction = 'descending';
    }
    @list.sort({ column, direction });
  };

  <div class={styles.Root}>
    <div class={styles.Header}>
      <button class={button.Root} onClick={() => handleSort('title')}>
        {'Sort by title '}
        if (!@list.sortDescriptor || @list.sortDescriptor.column !== 'title') {
          <ArrowUpDown />
        } else if (@list.sortDescriptor.direction === 'ascending') {
          <ArrowUp />
        } else {
          <ArrowDown />
        }
      </button>
      if (@list.loading) {
        <span class={styles.Loading}>
          <Loader class={styles.Spinner} />
          {'Loading'}
        </span>
      }
    </div>

    if (@list.error) {
      <div class={styles.Error}>
        {'Error: '}
        {@list.error.message}
      </div>
    }

    <div class={styles.ItemGroup}>
      for (const product of @list.items; key product.id) {
        <div class={styles.Item} data-variant="outline">
          <div class={styles.ItemMedia}>
            <img src={product.image} alt={product.title} />
          </div>
          <div class={styles.ItemContent}>
            <div class={styles.ItemTitle}>{product.title}</div>
            <div class={styles.ItemDescription}>
              {'

### Dependencies

Automatically reload data when dependencies change, such as filter selections or external state.

**Example: async-list/dependencies**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { Loader } from 'lucide-ripple';
import { track } from 'ripple';
import field from 'styles/field.module.css';
import styles from 'styles/async-list.module.css';

const LIMIT = 5;

interface User {
  id: number;
  name: string;
  email: string;
  department: string;
  role: string;
}

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const departments = ['Engineering', 'Marketing', 'Sales', 'Support'];

const roles = [
  'Senior Developer',
  'Marketing Manager',
  'Frontend Developer',
  'Sales Representative',
  'DevOps Engineer',
  'Customer Success',
  'Content Creator',
  'Backend Developer',
  'Account Manager',
  'Technical Support',
  'Brand Manager',
  'Full Stack Developer',
  'Sales Director',
  'Support Manager',
  'UI Designer',
  'Digital Marketer',
  'Mobile Developer',
  'Business Development',
  'Documentation Specialist',
  'Social Media Manager',
];

const mockUsers: User[] = [
  {
    id: 1,
    name: 'Alice Johnson',
    email: 'alice@example.com',
    department: 'Engineering',
    role: 'Senior Developer',
  },
  {
    id: 2,
    name: 'Bob Smith',
    email: 'bob@example.com',
    department: 'Marketing',
    role: 'Marketing Manager',
  },
  {
    id: 3,
    name: 'Carol Davis',
    email: 'carol@example.com',
    department: 'Engineering',
    role: 'Frontend Developer',
  },
  {
    id: 4,
    name: 'David Wilson',
    email: 'david@example.com',
    department: 'Sales',
    role: 'Sales Representative',
  },
  {
    id: 5,
    name: 'Eva Brown',
    email: 'eva@example.com',
    department: 'Engineering',
    role: 'DevOps Engineer',
  },
  {
    id: 6,
    name: 'Frank Miller',
    email: 'frank@example.com',
    department: 'Support',
    role: 'Customer Success',
  },
  {
    id: 7,
    name: 'Grace Lee',
    email: 'grace@example.com',
    department: 'Marketing',
    role: 'Content Creator',
  },
  {
    id: 8,
    name: 'Henry Taylor',
    email: 'henry@example.com',
    department: 'Engineering',
    role: 'Backend Developer',
  },
  {
    id: 9,
    name: 'Ivy Anderson',
    email: 'ivy@example.com',
    department: 'Sales',
    role: 'Account Manager',
  },
  {
    id: 10,
    name: 'Jack Thompson',
    email: 'jack@example.com',
    department: 'Support',
    role: 'Technical Support',
  },
  {
    id: 11,
    name: 'Kate Martinez',
    email: 'kate@example.com',
    department: 'Marketing',
    role: 'Brand Manager',
  },
  {
    id: 12,
    name: 'Liam Garcia',
    email: 'liam@example.com',
    department: 'Engineering',
    role: 'Full Stack Developer',
  },
  {
    id: 13,
    name: 'Mia Rodriguez',
    email: 'mia@example.com',
    department: 'Sales',
    role: 'Sales Director',
  },
  {
    id: 14,
    name: 'Noah Lopez',
    email: 'noah@example.com',
    department: 'Support',
    role: 'Support Manager',
  },
  {
    id: 15,
    name: 'Olivia White',
    email: 'olivia@example.com',
    department: 'Engineering',
    role: 'UI Designer',
  },
  {
    id: 16,
    name: 'Paul Harris',
    email: 'paul@example.com',
    department: 'Marketing',
    role: 'Digital Marketer',
  },
  {
    id: 17,
    name: 'Quinn Clark',
    email: 'quinn@example.com',
    department: 'Engineering',
    role: 'Mobile Developer',
  },
  {
    id: 18,
    name: 'Ruby Lewis',
    email: 'ruby@example.com',
    department: 'Sales',
    role: 'Business Development',
  },
  {
    id: 19,
    name: 'Sam Young',
    email: 'sam@example.com',
    department: 'Support',
    role: 'Documentation Specialist',
  },
  {
    id: 20,
    name: 'Tina Walker',
    email: 'tina@example.com',
    department: 'Marketing',
    role: 'Social Media Manager',
  },
];

export component Dependencies() {
  let selectedDepartment = track('');
  let selectedRole = track('');

  const dependencies = track(() => [@selectedDepartment, @selectedRole]);

  const list = useAsyncList<User>(
    {
      initialItems: mockUsers.slice(0, LIMIT),
      dependencies,
      async load({ filterText }: { filterText?: string }) {
        await delay(400);

        let items = mockUsers;

        if (@selectedDepartment) {
          items = items.filter((user) => user.department === @selectedDepartment);
        }

        if (@selectedRole) {
          items = items.filter((user) => user.role === @selectedRole);
        }

        if (filterText) {
          items = items.filter(
            (user) => user.name.toLowerCase().includes(filterText.toLowerCase()) ||
              user.email.toLowerCase().includes(filterText.toLowerCase()),
          );
        }

        return { items: items.slice(0, LIMIT) };
      },
    },
  );

  <div class={styles.Root}>
    <div class={styles.Header}>
      <select
        class={field.Select}
        value={@selectedDepartment}
        onChange={(e) => {
          @selectedDepartment = (e.target as HTMLSelectElement).value;
        }}
      >
        <option value="">{'All Departments'}</option>
        for (const dept of departments; key dept) {
          <option value={dept}>{dept}</option>
        }
      </select>

      <select
        class={field.Select}
        value={@selectedRole}
        onChange={(e) => {
          @selectedRole = (e.target as HTMLSelectElement).value;
        }}
      >
        <option value="">{'All Roles'}</option>
        for (const role of roles; key role) {
          <option value={role}>{role}</option>
        }
      </select>

      <input
        class={field.Input}
        type="text"
        placeholder="Search..."
        value={@list.filterText}
        onInput={(e) => {
          @list.setFilterText((e.target as HTMLInputElement).value);
        }}
      />

      if (@list.loading) {
        <span class={styles.Loading}>
          <Loader class={styles.Spinner} />
          {'Loading'}
        </span>
      }
    </div>

    if (@list.error) {
      <div class={styles.Error}>
        {'Error: '}
        {@list.error.message}
      </div>
    }

    <div class={styles.Status}>
      {'Found '}
      {@list.items.length}
      {' users'}
    </div>

    <div class={styles.ItemGroup}>
      for (const user of @list.items; key user.id) {
        <div class={styles.Item}>
          <div class={styles.ItemContent}>
            <div class={styles.ItemTitle}>{user.name}</div>
            <div class={styles.ItemDescription}>{user.email}</div>
            <div class={styles.ItemMeta}>
              {user.department}
              {' • '}
              {user.role}
            </div>
          </div>
        </div>
      }
    </div>

    if (@list.items.length === 0 && !@list.loading) {
      <div class={styles.Empty}>{'No users found with current filters'}</div>
    }
  </div>
}

```

## API Reference

### Props

- **load** (`(params: LoadParams<C>) => Promise<LoadResult<T, C>>`) - Function to load data asynchronously
- **sort** (`(params: SortParams<T>) => Promise<SortResult<T>> | SortResult<T>`) - Optional function for client-side
  sorting
- **autoReload** (`boolean`, default: `false`) - Whether to automatically reload data on mount
- **initialItems** (`T[]`, default: `[]`) - Initial items to display before first load
- **dependencies** (`any[]`, default: `[]`) - Values that trigger a reload when changed
- **initialFilterText** (`string`, default: `''`) - Initial filter text value
- **initialSortDescriptor** (`SortDescriptor | null`) - Initial sort configuration

### Load Parameters

The `load` function receives an object with the following properties:

- **cursor** (`C | undefined`) - Current cursor for pagination
- **filterText** (`string`) - Current filter text
- **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration
- **signal** (`AbortSignal`) - AbortController signal for request cancellation

### Load Result

The `load` function should return an object with:

- **items** (`T[]`) - The loaded items
- **cursor** (`C | undefined`) - Optional cursor for next page

### Sort Parameters

The `sort` function receives an object with:

- **items** (`T[]`) - Current items to sort
- **descriptor** (`SortDescriptor`) - Sort configuration with `column` and `direction`

### Return Value

The hook returns an object with the following properties and methods:

#### State Properties

- **items** (`T[]`) - Current list of items
- **loading** (`boolean`) - Whether a load operation is in progress
- **error** (`Error | null`) - Any error from the last operation
- **cursor** (`C | undefined`) - Current cursor for pagination
- **filterText** (`string`) - Current filter text
- **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration

#### Methods

- **reload** (`() => void`) - Reload data from the beginning
- **loadMore** (`() => void`) - Load more items (when cursor is available)
- **setFilterText** (`(text: string) => void`) - Set filter text and reload
- **sort** (`(descriptor: SortDescriptor) => void`) - Apply sorting

#### Types

```tsx
interface SortDescriptor {
  column: string
  direction: 'ascending' | 'descending'
}

interface LoadParams<C> {
  cursor?: C
  filterText: string
  sortDescriptor?: SortDescriptor | null
  signal: AbortSignal
}

interface LoadResult<T, C> {
  items: T[]
  cursor?: C
}
```}
              {product.price}
            </div>
            <div class={styles.ItemMeta}>
              {product.category}
              {' • '}
              {product.rating.rate}
              {' ('}
              {product.rating.count}
              {' reviews)'}
            </div>
          </div>
        </div>
      }
    </div>
  </div>
}

```

### Dependencies

Automatically reload data when dependencies change, such as filter selections or external state.

<Example id="async-list/dependencies" component="collection" />

## API Reference

### Props

- **load** (`(params: LoadParams<C>) => Promise<LoadResult<T, C>>`) - Function to load data asynchronously
- **sort** (`(params: SortParams<T>) => Promise<SortResult<T>> | SortResult<T>`) - Optional function for client-side
  sorting
- **autoReload** (`boolean`, default: `false`) - Whether to automatically reload data on mount
- **initialItems** (`T[]`, default: `[]`) - Initial items to display before first load
- **dependencies** (`any[]`, default: `[]`) - Values that trigger a reload when changed
- **initialFilterText** (`string`, default: `''`) - Initial filter text value
- **initialSortDescriptor** (`SortDescriptor | null`) - Initial sort configuration

### Load Parameters

The `load` function receives an object with the following properties:

- **cursor** (`C | undefined`) - Current cursor for pagination
- **filterText** (`string`) - Current filter text
- **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration
- **signal** (`AbortSignal`) - AbortController signal for request cancellation

### Load Result

The `load` function should return an object with:

- **items** (`T[]`) - The loaded items
- **cursor** (`C | undefined`) - Optional cursor for next page

### Sort Parameters

The `sort` function receives an object with:

- **items** (`T[]`) - Current items to sort
- **descriptor** (`SortDescriptor`) - Sort configuration with `column` and `direction`

### Return Value

The hook returns an object with the following properties and methods:

#### State Properties

- **items** (`T[]`) - Current list of items
- **loading** (`boolean`) - Whether a load operation is in progress
- **error** (`Error | null`) - Any error from the last operation
- **cursor** (`C | undefined`) - Current cursor for pagination
- **filterText** (`string`) - Current filter text
- **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration

#### Methods

- **reload** (`() => void`) - Reload data from the beginning
- **loadMore** (`() => void`) - Load more items (when cursor is available)
- **setFilterText** (`(text: string) => void`) - Set filter text and reload
- **sort** (`(descriptor: SortDescriptor) => void`) - Apply sorting

#### Types

```tsx
interface SortDescriptor {
  column: string
  direction: 'ascending' | 'descending'
}

interface LoadParams<C> {
  cursor?: C
  filterText: string
  sortDescriptor?: SortDescriptor | null
  signal: AbortSignal
}

interface LoadResult<T, C> {
  items: T[]
  cursor?: C
}
```