# Example: One result set, three claims about how many rows it has

The radio buttons change nothing about the records — the same 480 orders come back every time — only how sure the server says it is of the count. Watch what that alone changes: what an export may call all rows, and what the grid is willing to announce as aria-rowcount.

Source: https://pretable.ai/examples/server-totals.md

```tsx TotalsGrid.tsx
"use client";

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

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

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

const KINDS: TotalKind[] = ["exact", "estimate", "unknown"];

/** The query never changes here; only the confidence of the count does. */
const EMPTY_QUERY: OrderQuery = { filters: [], sort: [], rowGroups: [] };

const PROCESSING = { filter: "external", sort: "external" } as const;

/**
 * The same population whichever total is claimed: the radio buttons change the
 * server's confidence, not which records matched, so the key must NOT change
 * with them.
 */
const DATASET_KEY = JSON.stringify(EMPTY_QUERY);

const VIEWPORT_HEIGHT = 320;

export function TotalsGrid() {
  const [totalKind, setTotalKind] = useState<TotalKind>("exact");
  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 request is `loading`; every
  // radio press after it is `stale` — the rows stay up, untouched, while the
  // new count is in flight.
  const hasCommitted = useRef(false);

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

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

    fetchRows(EMPTY_QUERY, { totalKind }).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;
        setDataState({
          phase: "error",
          message: error instanceof Error ? error.message : "Request failed",
        });
      },
    );

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

  // The function the CSV export calls, given what this grid could give it:
  // how many records are loaded, what the server claims exists, and who has
  // authority over which records those are.
  const scope = resolveDataScope(
    { loadedRowCount: rows.length, matchingTotal: total },
    PROCESSING,
  );

  const [gridRef, ariaRowCount] = useAnnouncedRowCount();

  return (
    <div>
      <fieldset
        style={{
          border: 0,
          display: "flex",
          flexWrap: "wrap",
          gap: 12,
          margin: "0 0 8px",
          padding: 0,
        }}
      >
        <legend style={{ fontSize: 13, padding: 0 }}>
          The count the server claims
        </legend>
        {KINDS.map((kind) => (
          <label key={kind} style={{ fontSize: 13 }}>
            <input
              checked={totalKind === kind}
              name="server-totals-kind"
              onChange={() => setTotalKind(kind)}
              type="radio"
              value={kind}
            />{" "}
            {kind}
          </label>
        ))}
      </fieldset>
      <p role="status" style={{ margin: "0 0 8px", fontSize: 13 }}>
        Reported:{" "}
        <code data-testid="reported-total">{JSON.stringify(total)}</code> — an
        export of this grid would be scoped{" "}
        <code data-testid="export-scope">{scope}</code>, and the grid announces{" "}
        <code>aria-rowcount</code>{" "}
        <code data-testid="aria-rowcount">{ariaRowCount ?? "…"}</code>. The same
        480 records every time: only the claim about them changed.
      </p>
      <div ref={gridRef}>
        <PretableSurface<Order>
          ariaLabel="Orders"
          columns={columns}
          dataState={dataState}
          getRowId={(row) => row.id}
          // Fixed for the life of this grid, and the reason the funnels are
          // switched off: under external filter authority the engine stops
          // applying `query.filters`, so a funnel here would set one that did
          // nothing. What the claim changes on this page is what the grid is
          // willing to SAY about how many records there are.
          processing={PROCESSING}
          resultMeta={{ total, datasetKey: DATASET_KEY }}
          rows={rows}
          viewportHeight={VIEWPORT_HEIGHT}
        />
      </div>
    </div>
  );
}

/**
 * Reads the `aria-rowcount` the grid actually published, rather than
 * recomputing what it ought to be — the point of the readout is that it is the
 * attribute a screen reader gets.
 *
 * A `MutationObserver` rather than an effect keyed on the total, because the
 * attribute settles a beat after the rows commit: the row model ingests new
 * rows asynchronously, so the count is derived twice per change and only the
 * second value is the final one.
 */
function useAnnouncedRowCount(): [
  React.RefObject<HTMLDivElement | null>,
  string | null,
] {
  const ref = useRef<HTMLDivElement>(null);
  const [rowCount, setRowCount] = useState<string | null>(null);

  useEffect(() => {
    const viewport = ref.current?.querySelector(
      "[data-pretable-scroll-viewport]",
    );
    if (!viewport) return;

    const read = (): void =>
      setRowCount(viewport.getAttribute("aria-rowcount"));

    read();
    const observer = new MutationObserver(read);
    observer.observe(viewport, { attributeFilter: ["aria-rowcount"] });

    return () => observer.disconnect();
  }, []);

  return [ref, rowCount];
}
```

```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 for `isAnyOf`.
 *
 * Filtering and sorting are switched off in this grid, and only in this one.
 * Its query never leaves the client — the radio buttons change the TOTAL, not
 * the request's query — so a funnel here would filter locally while
 * `processing` claims the server chose the records, and an exact total would
 * go on announcing 481 rows over a body showing thirty. That is precisely the
 * dishonesty this page is about, so the example does not offer it. A real
 * server-filtered grid puts the query on the wire and leaves both on — that is
 * the grid on the section overview.
 */
export const columns: PretableColumn<Order>[] = [
  {
    id: "customer",
    header: "Customer",
    widthPx: 150,
    filterable: false,
    sortable: false,
  },
  {
    id: "region",
    header: "Region",
    type: "enum",
    widthPx: 90,
    filterable: false,
    sortable: false,
    options: [
      { value: "North" },
      { value: "South" },
      { value: "East" },
      { value: "West" },
    ],
  },
  {
    id: "status",
    header: "Status",
    type: "enum",
    widthPx: 100,
    filterable: false,
    sortable: false,
    options: [
      { value: "open" },
      { value: "shipped" },
      { value: "delivered" },
      { value: "cancelled" },
    ],
  },
  {
    id: "total",
    header: "Total",
    type: "number",
    widthPx: 90,
    filterable: false,
    sortable: false,
  },
  {
    id: "placedAt",
    header: "Placed",
    type: "date",
    widthPx: 100,
    filterable: false,
    sortable: false,
  },
];
```

```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 shape the surface speaks, 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.
 */
export type OrderQuery = PretableQueryFor<PretableSurfaceQueryColumns<Order>>;

/** How sure the endpoint should claim to be about the count. */
export type TotalKind = "exact" | "estimate" | "unknown";

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

/**
 * Sends the query and receives rows plus a description of them. Note what is
 * NOT sent: no `limit`, so every matching record comes back and the loaded
 * records really are the whole population — which is the only situation where
 * an exact total can honestly say so.
 */
export async function fetchRows(
  query: OrderQuery,
  options: { totalKind: TotalKind },
): Promise<RowsResponse> {
  const response = await fetch("/api/docs/rows", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ query, totalKind: options.totalKind }),
  });

  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;
}
```
