# Example: A grid whose filtering and sorting happen on the server

Every header sort and column filter becomes one POST to /api/docs/rows with a 500ms delay, and the rows that come back were filtered and ordered there. The grid's job is to publish what the reader asked for and render the answer.

Source: https://pretable.ai/examples/server-data-overview.md

```tsx ServerDataGrid.tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

import {
  PretableSurface,
  type PretableDataState,
  type PretableMatchingTotal,
} from "@pretable/react";

import { columns } from "./columns";
import { fetchRows, type Order, type OrderQuery } from "./fetch-rows";

const EMPTY_QUERY: OrderQuery = { filters: [], sort: [], rowGroups: [] };

const VIEWPORT_HEIGHT = 320;

export function ServerDataGrid() {
  const [query, setQuery] = useState<OrderQuery>(EMPTY_QUERY);
  const [rows, setRows] = useState<Order[]>([]);
  const [total, setTotal] = useState<PretableMatchingTotal>({
    kind: "unknown",
  });
  const [dataState, setDataState] = useState<PretableDataState>({
    phase: "loading",
  });

  // Whether a result has ever committed. The first fetch is `loading` (there is
  // nothing on screen yet); every one after it is `stale` — the previous rows
  // stay up, and stay sortable, while the new ones are in flight.
  const hasCommitted = useRef(false);

  useEffect(() => {
    let cancelled = false;

    setDataState({ phase: hasCommitted.current ? "stale" : "loading" });

    fetchRows(query).then(
      (result) => {
        if (cancelled) return;
        hasCommitted.current = true;
        setRows(result.rows);
        setTotal(result.total);
        setDataState({ phase: "idle" });
      },
      (error: unknown) => {
        if (cancelled) return;
        hasCommitted.current = true;
        // Rows are left untouched: a failed request never discards the last
        // result that did answer.
        setDataState({
          phase: "error",
          message: error instanceof Error ? error.message : "Request failed",
        });
      },
    );

    return () => {
      cancelled = true;
    };
  }, [query]);

  const onQueryChange = useCallback((next: OrderQuery) => setQuery(next), []);

  return (
    <div>
      <p role="status" style={{ margin: "0 0 8px", fontSize: 13 }}>
        Phase: <code>{dataState.phase}</code>
        {dataState.phase === "error" ? ` — ${dataState.message}` : ""}. Click a
        header or open a funnel: each one is a request, and the server decides
        the rows.
      </p>
      <PretableSurface<Order>
        ariaLabel="Orders"
        columns={columns}
        dataState={dataState}
        getRowId={(row) => row.id}
        onQueryChange={onQueryChange}
        // Declares that the SERVER chose these records and their order. The
        // filter half is taken literally: the query below stays published —
        // the funnel shows it, `onQueryChange` reports it — and the engine
        // stops re-selecting rows with it, so the last result survives on
        // screen while the next one loads. It also buys honest counts, which
        // is the totals page.
        processing={{ filter: "external", sort: "external" }}
        query={query}
        resultMeta={{ total, datasetKey: JSON.stringify(query) }}
        rows={rows}
        viewportHeight={VIEWPORT_HEIGHT}
      />
    </div>
  );
}
```

```ts columns.ts
import type { PretableColumn } from "@pretable/react";

import type { Order } from "./fetch-rows";

/**
 * Every `type` here must agree with what the endpoint believes the column is:
 * the filter menu offers operators by type, and an operator the server's column
 * type cannot use comes back as a 500 rather than as an unfiltered grid.
 *
 * The two `enum` columns declare their `options` rather than letting the
 * checklist auto-derive. Under external filtering the rows on hand are one
 * server-chosen window, so derived values would offer whichever of them
 * happened to load as if it were the complete universe.
 */
export const columns: PretableColumn<Order>[] = [
  { id: "customer", header: "Customer", widthPx: 150 },
  {
    id: "region",
    header: "Region",
    type: "enum",
    widthPx: 90,
    options: [
      { value: "North" },
      { value: "South" },
      { value: "East" },
      { value: "West" },
    ],
  },
  {
    id: "status",
    header: "Status",
    type: "enum",
    widthPx: 100,
    options: [
      { value: "open" },
      { value: "shipped" },
      { value: "delivered" },
      { value: "cancelled" },
    ],
  },
  { id: "total", header: "Total", type: "number", widthPx: 90 },
  { id: "placedAt", header: "Placed", type: "date", widthPx: 100 },
];
```

```ts fetch-rows.ts
import type {
  PretableMatchingTotal,
  PretableQueryFor,
  PretableSurfaceQueryColumns,
} from "@pretable/react";

export interface Order {
  id: string;
  customer: string;
  region: string;
  status: string;
  total: number;
  placedAt: string;
}

/**
 * The query the surface hands back, typed once and shared by everything that
 * touches it.
 *
 * Not `PretableQueryFor<typeof columns>`: these are plain `PretableColumn<Order>`
 * descriptors with no `accessor` field, and `PretableQueryFor` needs one to
 * resolve a filter to anything but `never`. `PretableSurfaceQueryColumns<Order>`
 * is the shape `<PretableSurface>` itself falls back to for accessor-less
 * columns, so this is the type the `query` / `onQueryChange` pair actually
 * speaks.
 */
export type OrderQuery = PretableQueryFor<PretableSurfaceQueryColumns<Order>>;

export interface RowsResponse {
  rows: Order[];
  total: PretableMatchingTotal;
  datasetKey: string;
}

/**
 * The whole of the client's job: send the query, receive rows plus a
 * description of them. The grid never does this — it has no idea a network
 * exists.
 */
export async function fetchRows(
  query: OrderQuery,
  options: { totalKind?: "exact" | "estimate" | "unknown" } = {},
): Promise<RowsResponse> {
  const response = await fetch("/api/docs/rows", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ query, totalKind: options.totalKind ?? "exact" }),
  });

  if (!response.ok) {
    const problem = (await response.json().catch(() => null)) as {
      message?: string;
    } | null;

    throw new Error(problem?.message ?? "Order service unavailable");
  }

  return (await response.json()) as RowsResponse;
}
```
