# جدول البيانات: بيانات الخادم

> Page and sort the slotsmith data table on a server with manualPagination, manualSorting and rowCount, and show loading, error and empty states.

When a server pages and sorts the rows, give the data table one page at a time: set `manualPagination` and `manualSorting`, pass the page as `data` and the total as `rowCount`. The table reports page and sort changes; you fetch. The demo uses TanStack Query.

```tsx
import { keepPreviousData, QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { DataTable, type DataTablePaginationState, type DataTableSortingState } from "slotsmith/data-table";
import { fetchPeople } from "../shared/fake-api";
import { columns } from "../shared/people";

function PeopleTable() {
  const [pagination, setPagination] = useState<DataTablePaginationState>({ pageIndex: 0, pageSize: 5 });
  const [sorting, setSorting] = useState<DataTableSortingState>([]);
  const [failing, setFailing] = useState(false);

  const query = useQuery({
    // A new page or order is a new key, so it is a new request.
    queryKey: ["people", pagination, sorting],
    // `failing` is left out of the key on purpose: ticking it changes how the next request ends, not which page it asks for.
    queryFn: ({ signal }) => fetchPeople({ ...pagination, sorting }, { signal, fail: failing }),
    // The last page stays on screen while the next one loads.
    placeholderData: keepPreviousData,
    // A switch back to the tab would refetch and dim the table for no visible change.
    refetchOnWindowFocus: false,
  });

  // A failed request has no total: keep the last one, so the page count stays.
  const [lastTotal, setLastTotal] = useState(0);
  useEffect(() => {
    if (query.data) setLastTotal(query.data.total);
  }, [query.data]);
  const total = query.data?.total ?? lastTotal;

  return (
    <>
      <label style={{ display: "block", marginBlockEnd: "0.75rem" }}>
        <input type="checkbox" checked={failing} onChange={(event) => setFailing(event.target.checked)} /> Fail the next requests
      </label>
      <DataTable
        // Only the current page: the server has already sorted and cut it.
        data={query.data?.rows ?? []}
        columns={columns}
        getRowId={(person) => person.id}
        manualPagination
        manualSorting
        // The total across all pages, so the table can count them.
        rowCount={total}
        pagination={pagination}
        onPaginationChange={setPagination}
        sorting={sorting}
        onSortingChange={(next) => {
          setSorting(next);
          // A new order starts again from the first page.
          setPagination((current) => ({ ...current, pageIndex: 0 }));
        }}
        pageSizeOptions={[5, 10, 20]}
        // Skeleton rows only while there is nothing to show, as on the first load.
        loading={query.isPending}
        error={query.error}
        onRetry={() => query.refetch()}
        // Dim the rows of the last page until the new ones arrive, and tell
        // assistive technology the table is busy while any request runs.
        style={{ opacity: query.isPlaceholderData ? 0.6 : 1 }}
        aria-busy={query.isFetching || undefined}
      />
      <p dir="ltr">
        {`GET /people?page=${pagination.pageIndex + 1}&size=${pagination.pageSize}`}
        {sorting.map((sort) => `&sort=${sort.desc ? "-" : ""}${sort.id}`).join("")}
        {query.isFetching ? " · loading" : query.isError ? " · failed" : " · done"}
      </p>
    </>
  );
}

export default function ServerData() {
  // One client for the app; a real one sits at the root, next to your other providers.
  const [client] = useState(() => new QueryClient({ defaultOptions: { queries: { retry: false } } }));
  return (
    <QueryClientProvider client={client}>
      <PeopleTable />
    </QueryClientProvider>
  );
}
```

The API here is a stand-in that answers after 300 ms; its code is in the `fake-api.ts` tab. Move to another page or click a header: the line under the table shows the request. Tick the checkbox, change the page, and the table shows the error with a retry button.

## Paging and sorting on the server

The page and the sort are controlled state. Each change is a new request, and the table does no paging or sorting of its own.

- [`manualPagination`](https://slotsmith.dev/ar/components/data-table/api/#props) says `data` is already one page. [`rowCount`](https://slotsmith.dev/ar/components/data-table/api/#props) is the total across all pages; the table needs it to count the pages and to disable the next-page button on the last one.
- [`pagination`](https://slotsmith.dev/ar/components/data-table/api/#props) and [`onPaginationChange`](https://slotsmith.dev/ar/components/data-table/api/#props) hold `{ pageIndex, pageSize }`, with `pageIndex` counting from 0. See [pagination](https://slotsmith.dev/ar/components/data-table/guides/pagination/).
- [`manualSorting`](https://slotsmith.dev/ar/components/data-table/api/#props) says the rows arrive already sorted. [`sorting`](https://slotsmith.dev/ar/components/data-table/api/#props) and [`onSortingChange`](https://slotsmith.dev/ar/components/data-table/api/#props) hold a list of `{ id, desc }`; the headers still show the order and carry `aria-sort`. See [sorting and selection](https://slotsmith.dev/ar/components/data-table/guides/sorting-and-selection/).

The demo goes back to the first page when the sort changes. The table does not do that itself under manual pagination, so `onSortingChange` sets `pageIndex` to 0.

## Loading

[`loading`](https://slotsmith.dev/ar/components/data-table/api/#props) set to `true` replaces the body with skeleton rows, one per row of the page size. The root then has `data-status="loading"` and `aria-busy="true"`, and the skeleton rows are hidden from assistive technology. Each skeleton cell is the [`Skeleton`](https://slotsmith.dev/ar/components/data-table/api/#slot-Skeleton) slot.

The demo passes `loading` only while there is nothing to show. With `placeholderData: keepPreviousData`, TanStack Query keeps the last page as `data` while the next one loads, so `isPending` is true on the first load alone. The demo dims the table while `isPlaceholderData` is true, and sets `aria-busy` on the root while any request runs (a root prop you pass replaces the one the table sets). Without the placeholder, every page change would show skeleton rows.

## Errors and empty results

- [`error`](https://slotsmith.dev/ar/components/data-table/api/#props) set to any value but `null`, `undefined` or `false` shows the [`Error`](https://slotsmith.dev/ar/components/data-table/api/#slot-Error) slot in place of the rows: the [`error`](https://slotsmith.dev/ar/components/data-table/api/#label-error) label in a `role="alert"` element. The root has `data-status="error"`.
- [`onRetry`](https://slotsmith.dev/ar/components/data-table/api/#props) adds a button named by the [`retry`](https://slotsmith.dev/ar/components/data-table/api/#label-retry) label. The demo calls the query's `refetch`.
- A page with no rows shows the [`Empty`](https://slotsmith.dev/ar/components/data-table/api/#slot-Empty) slot with the [`empty`](https://slotsmith.dev/ar/components/data-table/api/#label-empty) label, and `data-status="empty"`.

`loading` wins over `error`, and `error` over empty. If a page other than the first comes back empty, for example after its last row was deleted, the table steps back a page and reports it through `onPaginationChange`.

## Other ways to fetch

Nothing in the table depends on TanStack Query. Any code that turns `{ pageIndex, pageSize, sorting }` into `{ rows, total }` fits: a `fetch` in an effect, SWR, or a loader from your framework. Pass its rows, its total, its pending flag and its error to the same four props.
