# Example: Reporting the query without controlling it

Sort a header or apply a funnel and watch the request counter rise by exactly one: this grid keeps its own query and only reports that it changed, so onQueryChange arrives with no query prop beside it. Filter Customer for the word fail to watch a failed request leave every row it already had on screen, with the filter that failed still showing in the funnel above them.

Source: https://pretable.ai/examples/server-query-ownership.md

```tsx NotifyOnlyGrid.tsx
"use client";

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

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

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

/** What the grid starts with, and so what the first request asks for. */
const EMPTY_QUERY: OrderQuery = { filters: [], sort: [], rowGroups: [] };

export function NotifyOnlyGrid() {
  const [rows, setRows] = useState<Order[]>([]);
  const [requests, setRequests] = useState(0);
  const [dataState, setDataState] = useState<PretableDataState>({
    phase: "loading",
  });

  // Whether a result has ever committed: the first fetch is `loading` (nothing
  // is on screen yet), every one after it is `stale`.
  const hasCommitted = useRef(false);

  // Notify-only hands you a callback, not an effect, so there is no cleanup to
  // run when a newer query supersedes an in-flight one. A sequence number does
  // the same job: a response that is no longer the latest request's is dropped
  // rather than committed out of order.
  const latestRequest = useRef(0);

  const load = useCallback((query: OrderQuery) => {
    const requestId = latestRequest.current + 1;
    latestRequest.current = requestId;
    setRequests(requestId);
    setDataState({ phase: hasCommitted.current ? "stale" : "loading" });

    fetchRows(query).then(
      (result) => {
        if (requestId !== latestRequest.current) return;
        hasCommitted.current = true;
        setRows(result.rows);
        setDataState({ phase: "idle" });
      },
      (error: unknown) => {
        if (requestId !== latestRequest.current) 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",
        });
      },
    );
  }, []);

  // The one request the grid cannot ask for: nothing changed yet, so
  // `onQueryChange` has not fired.
  useEffect(() => {
    load(EMPTY_QUERY);
  }, [load]);

  return (
    <div>
      <p role="status" style={{ margin: "0 0 8px", fontSize: 13 }}>
        Requests sent: <code data-testid="request-count">{requests}</code> —
        phase <code>{dataState.phase}</code>
        {dataState.phase === "error" ? ` (${dataState.message})` : ""}. The grid
        holds the query; it only tells you when it changed.
      </p>
      <Pretable<Order>
        ariaLabel="Orders"
        columns={columns}
        dataState={dataState}
        getRowId={(row) => row.id}
        // No `query` prop — `<Pretable>` does not accept one. The engine owns
        // the reader's intent and reports it here, which is enough to fetch
        // against and one prop less to keep in sync.
        onQueryChange={load}
        // Declares that the SERVER chose these records and their order. The
        // filter half is taken literally: the engine keeps reporting the query
        // above — the funnel shows it, `onQueryChange` fires with it — and
        // stops re-selecting rows with it, which is what keeps these rows on
        // screen while a request is in flight or has failed. The sort half is a
        // claim only; a header click still reorders them here.
        processing={{ filter: "external", sort: "external" }}
        rows={rows}
      />
    </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 — and the engine says
 * so, out loud, the first time such a funnel opens.
 */
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 grid reports, 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`. Both spellings compile, so nothing
 * would have told you — every filter would just be `never`.
 * `PretableSurfaceQueryColumns<Order>` is the shape the component itself falls
 * back to for accessor-less columns, so it is the type `onQueryChange` speaks.
 */
export type OrderQuery = PretableQueryFor<PretableSurfaceQueryColumns<Order>>;

export interface RowsResponse {
  rows: Order[];
  /**
   * How many records matched. This example never publishes it — with no
   * `resultMeta`, the grid counts the rows it holds — because notify-only is
   * the subject here. See the totals page for the honest version.
   */
  total: PretableMatchingTotal;
  datasetKey: string;
}

/**
 * The whole of the client's job: send the query, receive rows. The grid never
 * does this — it has no idea a network exists, which is why the query has to
 * reach you through a callback before anything can be fetched.
 */
export async function fetchRows(query: OrderQuery): Promise<RowsResponse> {
  const response = await fetch("/api/docs/rows", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ query, 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;
}
```
