---
id: "packages/active-grid/client-server-and-simulation"
title: "Client, server and simulation"
description: "Where the rows of a grid live, who sorts, filters and pages them, and what each of the three kinds of example on this site proves."
url: "/packages/active-grid/client-server-and-simulation"
locale: "en"
revision: "9ad12d330a8c13be"
---

# Client, server and simulation

A grid has its rows in one of two places. In **client mode** the browser holds every row and the
grid sorts, filters and pages them itself. In **server mode** the rows stay on a server: the grid
asks for one block at a time and says what to apply, and your server answers with that block.

The examples on this site come in three kinds, and each says what it proves.

| Kind | What runs | What it proves |
| --- | --- | --- |
| **Client** | all rows in the browser; the grid sorts, filters and pages | the grid's behaviour on data it holds |
| **Simulation** | a `fetchFn` with a delay over rows that are in the browser | the interaction of server mode, not a server |
| **Server** | a real route of this site that answers each request | the host contract of server mode, from end to end |

## Choosing a mode

| Question | Client mode | Server mode |
| --- | --- | --- |
| How many rows? | hundreds to a few thousand | any number: one block at a time |
| May the user see every row? | yes, they are all sent | only the rows your server returns |
| Who sorts, filters and searches? | the grid, in the browser | your server, from the grid's request |
| What does a filter change cost? | nothing: the rows are there | a request; the grid debounces and cancels |
| Totals and options of the select filters | computed by the grid | your server sends `totalCount` and `facets` |

Start in client mode when the data is small and the user may see all of it. Move to server mode as
soon as either is no longer true: the switch is the `mode` prop, a `fetchFn`, and the route behind
it. Column definitions, styling, selection and export stay the same.

## Client mode: the starting point

`mode="client"` with `data`: the grid holds every row. Sorting, the column filters, the global
search and paging all work on the rows it has. This is the smallest working grid on this site.

**Example: Orders in client mode**

Client: Runs entirely in your browser. Source: `examples/grid/client-orders.tsx`.

```tsx
// apps/docs/examples/grid/client-orders.tsx
'use client';

import '@workspace/active-grid/styles.css';

import {useMemo} from 'react';
import {generateOrders, type Order} from '@/lib/examples/orders';

import {
  composePresentationContext,
  toIanaTimeZone,
} from '@workspace/active-format';
import {
  ActiveGrid,
  createColumnHelper,
  englishGridTranslator,
  type GridColumnDef,
} from '@workspace/active-grid';

// The presentation: language, notation and the time zone the date filter
// needs for its relative presets and calendar.
const presentation = composePresentationContext(
  'en-GB',
  null,
  toIanaTimeZone('Europe/Amsterdam'),
);

const STATUS = [
  {label: 'Pending', value: 'pending'},
  {label: 'Processing', value: 'processing'},
  {label: 'Shipped', value: 'shipped'},
  {label: 'Delivered', value: 'delivered'},
  {label: 'Cancelled', value: 'cancelled'},
];

const ch = createColumnHelper<Order>();
const columns: GridColumnDef<Order>[] = [
  ch.accessor('id', {header: 'Order', meta: {filterType: 'text'}}),
  ch.accessor('customer', {header: 'Customer', meta: {filterType: 'text'}}),
  ch.accessor('category', {
    header: 'Category',
    meta: {filterType: 'select'},
  }),
  ch.accessor('quantity', {header: 'Qty', type: 'number'}),
  ch.accessor('total', {
    header: 'Total',
    type: 'currency',
    meta: {currencyCode: 'EUR', filterType: 'number'},
  }),
  ch.accessor('orderDate', {header: 'Ordered', type: 'date'}),
  ch.accessor('status', {
    header: 'Status',
    type: 'badge',
    meta: {
      filterType: 'select',
      filterOptions: STATUS,
      statusTones: {
        pending: 'warning',
        processing: 'info',
        shipped: 'info',
        delivered: 'success',
        cancelled: 'danger',
      },
    },
  }),
];

/** Client mode: all 500 rows are in the browser; the grid sorts, filters and pages them there. */
export default function ClientOrders() {
  const data = useMemo(() => generateOrders(500), []);
  return (
    <ActiveGrid<Order>
      gridId="docs-client-orders"
      mode="client"
      data={data}
      columns={columns}
      height={420}
      locale={presentation}
      t={englishGridTranslator}
      enableRowSelection
      pagination={{enabled: true, pageSizeOptions: [10, 25, 50]}}
      toolbar={{search: {enabled: true}, showColumnChooser: true}}
    />
  );
}
```

## Server mode against a real route

`mode="server"` with a `fetchFn`. The grid asks for **one block** of rows at a time and sends what
to apply: the block (`pageIndex` and `fetchSize`), the sorting, the column filters and the global
search. The server below holds 5,000 seeded orders that never leave it; it validates the request,
filters, searches, sorts and slices on the server, and answers with one block, the total count and
the facets for the select filters.

**Example: Server mode against a real route**

Server: Calls a real route of this site, which does the work on the server. Source: `examples/grid/server-orders.tsx`.

```tsx
// apps/docs/examples/grid/server-orders.tsx
'use client';

import '@workspace/active-grid/styles.css';

import {useCallback, useState} from 'react';
import type {Order} from '@/lib/examples/orders';

import {
  composePresentationContext,
  toIanaTimeZone,
} from '@workspace/active-format';
import {
  ActiveGrid,
  createColumnHelper,
  englishGridTranslator,
  globalField,
  scalarField,
  type GridColumnDef,
  type PagedResponse,
  type ServerFetchFunction,
} from '@workspace/active-grid';

const presentation = composePresentationContext(
  'en-GB',
  null,
  toIanaTimeZone('Europe/Amsterdam'),
);

const ch = createColumnHelper<Order>();
// In server mode the column contract (`server`) declares what the server can
// sort and filter; the grid offers exactly that, and nothing the server would
// refuse.
const columns: GridColumnDef<Order>[] = [
  ch.accessor('id', {
    header: 'Order',
    meta: {filterType: 'text'},
    server: {...scalarField(), global: globalField()},
  }),
  ch.accessor('customer', {
    header: 'Customer',
    meta: {filterType: 'text'},
    server: {...scalarField(), global: globalField()},
  }),
  ch.accessor('category', {
    header: 'Category',
    meta: {filterType: 'select'},
    server: {...scalarField(), global: globalField()},
  }),
  ch.accessor('total', {
    header: 'Total',
    type: 'currency',
    meta: {currencyCode: 'EUR', filterType: 'number'},
    server: scalarField(),
  }),
  ch.accessor('orderDate', {
    header: 'Ordered',
    type: 'date',
    meta: {filterType: 'date'},
    server: scalarField(),
  }),
  ch.accessor('status', {
    header: 'Status',
    meta: {filterType: 'select'},
    server: scalarField(),
  }),
];

interface Exchange {
  n: number;
  request: string;
  answer: string;
}

/**
 * Server mode against a real route of this site. The browser sends the
 * grid's request (page, block size, sorting, filters, search); the server
 * filters, sorts and pages 5,000 orders and answers with one block.
 */
export default function ServerOrders() {
  const [log, setLog] = useState<Exchange[]>([]);
  const fetchOrders = useCallback<ServerFetchFunction<Order>>(
    async ({signal, ...params}) => {
      const body = JSON.stringify(params);
      const response = await fetch('/api/examples/grid/orders', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body,
        signal,
      });
      const text = await response.text();
      if (!response.ok) throw new Error(`The server refused: ${text}`);
      const page = JSON.parse(text) as PagedResponse<Order>;
      setLog((list) =>
        [
          {
            n: (list[0]?.n ?? 0) + 1,
            request: body,
            answer: `${page.data.length} rows of ${page.totalCount}, ${(text.length / 1024).toFixed(1)} kB`,
          },
          ...list,
        ].slice(0, 5),
      );
      return page;
    },
    [],
  );
  return (
    <div className="flex flex-col gap-4">
      <ActiveGrid<Order>
        gridId="docs-server-orders"
        mode="server"
        fetchFn={fetchOrders}
        columns={columns}
        height={380}
        locale={presentation}
        t={englishGridTranslator}
        pagination={{enabled: true, pageSizeOptions: [10, 25, 50]}}
        toolbar={{search: {enabled: true}}}
        serverOptions={{blockSize: 50, retryCount: 0}}
      />
      <section aria-label="Requests to the server" data-request-log>
        <h3 className="mb-2 text-sm font-semibold">
          Requests (POST /api/examples/grid/orders)
        </h3>
        <ol className="flex flex-col gap-2">
          {log.map((exchange) => (
            <li
              key={exchange.n}
              className="bg-muted rounded-md p-2 font-mono text-xs break-all"
              data-request={exchange.n}
            >
              <div>
                #{exchange.n} → {exchange.request}
              </div>
              <div className="text-muted-foreground">← {exchange.answer}</div>
            </li>
          ))}
        </ol>
      </section>
    </div>
  );
}
```

Under the grid, the request window shows each request your browser sent and the size of the
answer. Try the combinations a user makes: filter on a status, sort by amount, search for a
customer, and page on. Each change is one request with the whole query, and one block comes back,
never the dataset.

### What the package does, and what your host does

| The grid (active-grid) | Your host |
| --- | --- |
| sends one request per change, with the block, sorting, filters and search | a route that validates every field of the request before it uses it |
| cancels a request that a newer one supersedes (`signal`) | passes the signal on to its own data access, so abandoned work stops |
| keeps the block, the total and the facets it receives | translates filters and sorting into a query of its data store |
| debounces typing in a filter or the search | decides who may read which rows and columns (authorization, tenancy) |
| shows loading, empty and error states | returns errors the grid can show, and never leaks the reason to a stranger |

### The server side

The route this example calls, and the module that holds the data and validates the request. Both
run only on the server.

`app/api/examples/grid/orders/route.ts`

```ts
// apps/docs/app/api/examples/grid/orders/route.ts

import {answerOrderQuery} from '@/lib/examples/orders-server';

/**
 * The server of the grid's server examples: one POST per block. The body is
 * the grid's fetch request (page index, block size, sorting, filters, global
 * search); the server filters, sorts and slices its seeded dataset and
 * answers with one block and the total count. Nothing is cached.
 */
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function POST(request: Request) {
  const body: unknown = await request.json().catch(() => null);
  const answer = answerOrderQuery(body);
  if (!answer.ok) {
    return Response.json({error: answer.error}, {status: 400});
  }
  return Response.json(answer.page, {
    headers: {'Cache-Control': 'no-store'},
  });
}
```

`lib/examples/orders-server.ts`

```ts
// apps/docs/lib/examples/orders-server.ts
import 'server-only';

import {z} from 'zod';

import {generateOrders, queryOrders} from './orders';

/**
 * The server side of the grid's server examples: 5,000 seeded orders that
 * exist only on this server, and the validation of a request before the
 * query runs. A browser never receives this dataset; it receives one block
 * of rows per request.
 */
const ORDERS = generateOrders(5000);

const filterValue = z.union([
  z.array(z.string()).max(50),
  z.object({
    operator: z.string().max(32),
    value: z.union([z.string().max(200), z.number(), z.boolean()]).optional(),
    valueTo: z.union([z.string().max(200), z.number()]).optional(),
  }),
]);

const orderQuery = z.object({
  pageIndex: z.number().int().min(0).max(10_000),
  fetchSize: z.number().int().min(1).max(500),
  sorting: z.array(z.object({id: z.string(), desc: z.boolean()})).max(5),
  filters: z.array(z.object({id: z.string(), value: filterValue})).max(20),
  globalFilter: z.string().max(200).optional(),
});

/** Validates `body` and runs the query; a request outside the contract is refused. */
export function answerOrderQuery(body: unknown) {
  const parsed = orderQuery.safeParse(body);
  if (!parsed.success) return {ok: false as const, error: 'Invalid request'};
  try {
    return {ok: true as const, page: queryOrders(ORDERS, parsed.data)};
  } catch (err) {
    return {
      ok: false as const,
      error: err instanceof RangeError ? err.message : 'Invalid request',
    };
  }
}
```

The query itself (filtering by the grid's operator forms, search, sorting and the block):

`lib/examples/orders.ts`

```ts
// apps/docs/lib/examples/orders.ts

/**
 * A deterministic, seeded dataset of orders for the grid examples, and the
 * query the server examples apply to it: filtering, global search, sorting
 * and one block of rows, in the shapes of the grid's `ServerFetchParams`
 * and `PagedResponse`.
 *
 * The generator and its filter semantics follow the seeded demo dataset of
 * the grid lab the examples came from (orders over six categories, a
 * mulberry32 generator with seed 123). Pure: no I/O, no clock, no
 * randomness beyond the seed, so every reader and every test sees the same
 * rows. The server examples keep the dataset on the server
 * (`orders-server.ts`); only the client and simulation examples build it in
 * the browser.
 */

export interface Order {
  id: string;
  customer: string;
  email: string;
  product: string;
  category: string;
  quantity: number;
  unitPrice: number;
  total: number;
  /** A calendar day, `yyyy-MM-dd`. */
  orderDate: string;
  status: OrderStatus;
  priority: 'low' | 'medium' | 'high' | 'urgent';
}

export type OrderStatus =
  'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

export const ORDER_STATUSES: readonly OrderStatus[] = [
  'pending',
  'processing',
  'shipped',
  'delivered',
  'cancelled',
];

export const CATEGORIES = [
  'Electronics',
  'Clothing',
  'Home',
  'Books',
  'Sports',
  'Food',
] as const;

const PRODUCTS: Record<(typeof CATEGORIES)[number], string[]> = {
  Electronics: ['Wireless Headphones', 'USB-C Hub', 'Laptop Stand', 'Webcam'],
  Clothing: ['Cotton T-Shirt', 'Denim Jeans', 'Winter Jacket', 'Wool Sweater'],
  Home: ['Desk Lamp', 'Wall Clock', 'Kitchen Scale', 'Plant Pot'],
  Books: [
    'TypeScript Handbook',
    'Design Patterns',
    'Clean Code',
    'UX Research',
  ],
  Sports: ['Yoga Mat', 'Water Bottle', 'Tennis Racket', 'Foam Roller'],
  Food: ['Organic Coffee', 'Green Tea', 'Dark Chocolate', 'Olive Oil'],
};

const FIRST_NAMES = ['Emma', 'Liam', 'Olivia', 'Noah', 'Ava', 'Ethan', 'Mia'];
const LAST_NAMES = ['Smith', 'Garcia', 'Jansen', 'Kowalski', 'Haddad', 'Sato'];

/** mulberry32: a small, seeded generator; the same seed gives the same rows. */
function mulberry32(seed: number): () => number {
  let state = seed;
  return () => {
    state |= 0;
    state = (state + 0x6d2b79f5) | 0;
    let t = Math.imul(state ^ (state >>> 15), 1 | state);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function pick<T>(list: readonly T[], rand: () => number): T {
  return list[Math.floor(rand() * list.length)]!;
}

/** `count` orders, always the same for the same count. */
export function generateOrders(count: number): Order[] {
  const rand = mulberry32(123);
  const orders: Order[] = [];
  for (let i = 0; i < count; i += 1) {
    const first = pick(FIRST_NAMES, rand);
    const last = pick(LAST_NAMES, rand);
    const category = pick(CATEGORIES, rand);
    const product = pick(PRODUCTS[category], rand);
    const quantity = 1 + Math.floor(rand() * 50);
    const unitPrice = Math.round((5 + rand() * 994) * 100) / 100;
    const year = 2024 + Math.floor(rand() * 3);
    const month = 1 + Math.floor(rand() * 12);
    const day = 1 + Math.floor(rand() * 28);
    const s = rand();
    const p = rand();
    orders.push({
      id: `ORD-${String(i + 1).padStart(5, '0')}`,
      customer: `${first} ${last}`,
      email: `${first.toLowerCase()}.${last.toLowerCase()}@example.com`,
      product,
      category,
      quantity,
      unitPrice,
      total: Math.round(quantity * unitPrice * 100) / 100,
      orderDate: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
      status:
        s < 0.15
          ? 'pending'
          : s < 0.35
            ? 'processing'
            : s < 0.6
              ? 'shipped'
              : s < 0.9
                ? 'delivered'
                : 'cancelled',
      priority:
        p < 0.35 ? 'low' : p < 0.7 ? 'medium' : p < 0.9 ? 'high' : 'urgent',
    });
  }
  return orders;
}

/** One column filter as the grid sends it: an operator form or a selection. */
export type OrderFilterValue =
  | string[]
  | {
      operator: string;
      value?: string | number | boolean;
      valueTo?: string | number;
    };

/** What a fetch asks for: the serialisable part of the grid's `ServerFetchParams`. */
export interface OrderQuery {
  pageIndex: number;
  fetchSize: number;
  sorting: {id: string; desc: boolean}[];
  /** As the grid sends them; a value outside `OrderFilterValue` filters nothing. */
  filters: {id: string; value: unknown}[];
  globalFilter?: string;
}

/** One block of rows, in the shape of the grid's `PagedResponse`. */
export interface OrderPage {
  data: Order[];
  totalCount: number;
  facets: Record<string, {label: string; value: string; count: number}[]>;
}

const SEARCHED: (keyof Order)[] = [
  'id',
  'customer',
  'email',
  'product',
  'category',
];
const COLUMNS = new Set<string>([
  'id',
  'customer',
  'email',
  'product',
  'category',
  'quantity',
  'unitPrice',
  'total',
  'orderDate',
  'status',
  'priority',
]);

function isOperatorForm(
  value: unknown,
): value is Exclude<OrderFilterValue, string[]> {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as {operator?: unknown}).operator === 'string'
  );
}

function matches(cell: unknown, filter: unknown): boolean {
  if (Array.isArray(filter)) {
    return filter.length === 0 || filter.map(String).includes(String(cell));
  }
  if (!isOperatorForm(filter)) return true;
  const {operator, value, valueTo} = filter;
  if (operator === 'isEmpty') return cell === null || cell === '';
  if (operator === 'isNotEmpty') return cell !== null && cell !== '';
  if (value === undefined || value === '') return true;
  if (typeof cell === 'number') {
    const n = Number(value);
    switch (operator) {
      case 'equals':
        return cell === n;
      case 'notEquals':
        return cell !== n;
      case 'greaterThan':
        return cell > n;
      case 'greaterThanOrEqual':
        return cell >= n;
      case 'lessThan':
        return cell < n;
      case 'lessThanOrEqual':
        return cell <= n;
      case 'between':
      case 'inRange':
        return cell >= n && cell <= Number(valueTo);
      default:
        return true;
    }
  }
  const text = String(cell ?? '');
  const wanted = String(value);
  // A calendar day compares as text: `yyyy-MM-dd` sorts as it reads.
  switch (operator) {
    case 'onDate':
      return text === wanted.slice(0, 10);
    case 'before':
      return text < wanted.slice(0, 10);
    case 'after':
      return text > wanted.slice(0, 10);
    case 'between':
    case 'inRange':
      return (
        text >= wanted.slice(0, 10) && text <= String(valueTo).slice(0, 10)
      );
  }
  const a = text.toLowerCase();
  const b = wanted.toLowerCase();
  switch (operator) {
    case 'contains':
      return a.includes(b);
    case 'notContains':
      return !a.includes(b);
    case 'startsWith':
      return a.startsWith(b);
    case 'endsWith':
      return a.endsWith(b);
    case 'equals':
      return a === b;
    case 'notEquals':
      return a !== b;
    default:
      return true;
  }
}

function facets(rows: Order[], field: 'category' | 'status' | 'priority') {
  const counts = new Map<string, number>();
  for (const row of rows)
    counts.set(row[field], (counts.get(row[field]) ?? 0) + 1);
  return [...counts]
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([value, count]) => ({label: value, value, count}));
}

/**
 * Filters, searches, sorts and returns one block of `orders`. Unknown
 * columns in a filter or sort are refused, as a host refuses anything its
 * contract does not name.
 */
export function queryOrders(
  orders: readonly Order[],
  query: OrderQuery,
): OrderPage {
  for (const item of [...query.filters, ...query.sorting]) {
    if (!COLUMNS.has(item.id))
      throw new RangeError(`Unknown column ${item.id}`);
  }
  let rows = [...orders];
  const search = query.globalFilter?.trim().toLowerCase();
  if (search) {
    rows = rows.filter((row) =>
      SEARCHED.some((field) =>
        String(row[field]).toLowerCase().includes(search),
      ),
    );
  }
  for (const filter of query.filters) {
    rows = rows.filter((row) =>
      matches(row[filter.id as keyof Order], filter.value),
    );
  }
  if (query.sorting.length > 0) {
    rows.sort((a, b) => {
      for (const sort of query.sorting) {
        const x = a[sort.id as keyof Order];
        const y = b[sort.id as keyof Order];
        const order =
          typeof x === 'number' && typeof y === 'number'
            ? x - y
            : String(x) < String(y)
              ? -1
              : String(x) > String(y)
                ? 1
                : 0;
        if (order !== 0) return sort.desc ? -order : order;
      }
      return 0;
    });
  }
  const start = query.pageIndex * query.fetchSize;
  return {
    data: rows.slice(start, start + query.fetchSize),
    totalCount: rows.length,
    facets: {
      category: facets(rows, 'category'),
      status: facets(rows, 'status'),
      priority: facets(rows, 'priority'),
    },
  };
}
```

### What this example proves, and what it does not

**It proves** that the grid's server mode works against a real HTTP route of a host: the request
shape the grid sends, the response shape it needs (`data`, `totalCount`, `facets`), cancellation of
a superseded request through the `AbortSignal`, and that only one block reaches the browser.

**It does not prove:**

- **your database**: the translation of filters and sorting into a query of your data store, its
  indexes and its query plans. The seeded dataset is filtered in memory;
- **authorization and tenancy**: this route answers anyone; your route decides who may see which
  rows and columns;
- **production load**: real latency, concurrency, large result sets and caching;
- **your serialization**: decimals and dates from a database (here every value is already a plain
  JSON number or a `yyyy-MM-dd` text).

## Simulation: the states of server mode without a server

A `fetchFn` that waits and then answers from rows already in the browser. It is **not** a server:
nothing leaves the page. Use it to see what the grid does while it waits, when an answer is empty,
and when a request fails: the switch below the grid makes the next request fail.

**Example: Server mode against an imitated server**

Simulation: A server imitated in your browser: shows the interaction, not a working server integration. Source: `examples/grid/simulated-orders.tsx`.

```tsx
// apps/docs/examples/grid/simulated-orders.tsx
'use client';

import '@workspace/active-grid/styles.css';

import {useCallback, useMemo, useState} from 'react';
import {generateOrders, queryOrders, type Order} from '@/lib/examples/orders';

import {
  composePresentationContext,
  toIanaTimeZone,
} from '@workspace/active-format';
import {
  ActiveGrid,
  createColumnHelper,
  englishGridTranslator,
  scalarField,
  type GridColumnDef,
  type ServerFetchFunction,
} from '@workspace/active-grid';
import {
  Field,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Switch} from '@workspace/active-ui/components/primitives/switch';

const presentation = composePresentationContext(
  'en-GB',
  null,
  toIanaTimeZone('Europe/Amsterdam'),
);

const ch = createColumnHelper<Order>();
const columns: GridColumnDef<Order>[] = [
  ch.accessor('id', {
    header: 'Order',
    meta: {filterType: 'text'},
    server: scalarField(),
  }),
  ch.accessor('product', {
    header: 'Product',
    meta: {filterType: 'text'},
    server: scalarField(),
  }),
  ch.accessor('quantity', {
    header: 'Qty',
    type: 'number',
    server: scalarField(),
  }),
  ch.accessor('total', {
    header: 'Total',
    type: 'currency',
    meta: {currencyCode: 'EUR', filterType: 'number'},
    server: scalarField(),
  }),
];

const wait = (ms: number, signal: AbortSignal) =>
  new Promise<void>((resolve, reject) => {
    const timer = setTimeout(resolve, ms);
    signal.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(signal.reason);
    });
  });

/**
 * Server mode against a server imitated in the browser: a fetchFn with a
 * delay over rows that are already here, and a switch that makes it fail.
 * It shows the grid's loading, paging and error states, not a server.
 */
export default function SimulatedOrders() {
  const orders = useMemo(() => generateOrders(2000), []);
  const [failing, setFailing] = useState(false);
  const fetchOrders = useCallback<ServerFetchFunction<Order>>(
    async ({signal, ...params}) => {
      await wait(600, signal);
      if (failing) throw new Error('The simulated server is down');
      return queryOrders(orders, params);
    },
    [orders, failing],
  );
  return (
    <div className="flex flex-col gap-4">
      <Field orientation="horizontal">
        <Switch
          id="simulated-failure"
          // Named in the server HTML already, not only after hydration.
          aria-labelledby="simulated-failure-label"
          checked={failing}
          onCheckedChange={setFailing}
        />
        <FieldLabel id="simulated-failure-label" htmlFor="simulated-failure">
          Make the next request fail
        </FieldLabel>
      </Field>
      <ActiveGrid<Order>
        key={String(failing)}
        gridId="docs-simulated-orders"
        mode="server"
        fetchFn={fetchOrders}
        columns={columns}
        height={360}
        locale={presentation}
        t={englishGridTranslator}
        pagination={{enabled: true}}
        serverOptions={{blockSize: 25, retryCount: 0}}
      />
    </div>
  );
}
```

## The request contract in short

| The grid sends ([`ServerFetchParams`](/packages/active-grid/server-api#serverfetchparams)) | Your server answers ([`PagedResponse`](/packages/active-grid/server-api#pagedresponse)) |
| --- | --- |
| `pageIndex`, `fetchSize`: one block, not the UI page | `data`: the rows of that block |
| `sorting`: `[{id, desc}]` | `totalCount`: the number of rows after filtering |
| `filters`: `[{id, value}]` in the operator forms of `FilterValue` | `facets` (optional): options with counts per column |
| `globalFilter`: the search text | `aggregations` (optional): footer values |
| `signal`: aborted when a newer request supersedes this one | groups, for server grouping |

The function itself is a
[`ServerFetchFunction`](/packages/active-grid/server-api#serverfetchfunction); what each column may
ask of the server (sort, filter, search, group) is its
[column query contract](/packages/active-grid/server-api#servercolumncontract).

## Next

- [Server mode](/packages/active-grid/server-mode): blocks, the request parameters, mutations and
  the patterns to avoid.
- [Server grouping](/packages/active-grid/server-grouping): groups and their totals from the
  server.
- [Filtering](/packages/active-grid/filters): the filter types and the operator forms a server
  receives.
- [Selection](/packages/active-grid/selection): select-all in server mode without loading every row.
- [Export](/packages/active-grid/export): which rows a file holds, and the formats.
