ഉള്ളടക്കത്തിലേക്ക് പോകുക
ബീറ്റ · പരീക്ഷണാത്മകംReact 19പിയർ ഡിപൻഡൻസികളില്ല

Sorting, Filtering & Server Data

Sorting, filtering, search, row selection, controlled state and server-side pagination.

ബീറ്റ കമ്പോണന്റുകൾ മാറാൻ സാധ്യതയുണ്ട്; അവ നിങ്ങളുടെ കോഡ് തകരാറിലാക്കിയേക്കാം. സ്വന്തം ഉത്തരവാദിത്തത്തിൽ ഉപയോഗിക്കുക, ബഗ് ട്രാക്കർ വഴി അഭിപ്രായം അറിയിക്കുക.

ഈ പേജ് ഇതുവരെ വിവർത്തനം ചെയ്തിട്ടില്ല, അതിനാൽ ഇംഗ്ലീഷ് പതിപ്പാണ് താഴെ കാണിക്കുന്നത്.
ഈ പേജിൽ

How rows get into the grid and how they are narrowed down — in client mode, or with a server holding the data. Start at Data Grid for installation and the props table.

  • Sorting: click a header to cycle ascending → descending → off. Shift + click adds the column to a multi-column sort. Empty values always sort last. Text sorts naturally ("Item 2" before "Item 10") and ignores case.
  • Column filters: open the column menu (the ⋮ button on the header, or Alt + ↓ on a focused header). A filter with an empty value is ignored. All column filters must match.
  • Search: the toolbar search matches the displayed text of every searchable column. Every word must match somewhere in the row, so paris active finds rows containing both.
  • Changing filters, search or sorting returns to the first page.

Filter operators

Column typeOperatorsValue format
stringcontains, notContains, equals, notEquals, startsWith, endsWith, isEmpty, isNotEmptytext, case-insensitive
numberequals, notEquals, gt, gte, lt, lte, between, isEmpty, isNotEmptynumber; between uses value and value2 (both inclusive, either optional)
dateequals (same day), before, after, between, isEmpty, isNotEmpty"YYYY-MM-DD", compared as local calendar days
booleanequalstrue or false
any column with optionsin ("is any of"), isEmpty, isNotEmptyarray of option values

Set filters from code:

tsx
gridRef.current?.setFilter("department", {
  operator: "in",
  value: ["Engineering", "Sales"],
});
gridRef.current?.setFilter("salary", {
  operator: "between",
  value: 50000,
  value2: 90000,
});
gridRef.current?.setFilter("salary", null); // remove

or start with them:

tsx
<DataGrid
  initialState={{
    sorting: [{ columnId: "salary", desc: true }],
    filters: [{ columnId: "active", operator: "equals", value: true }],
    globalFilter: "",
    pagination: { pageIndex: 0, pageSize: 25 },
  }}
/>

Row Selection

tsx
const gridRef = useRef<GridApi<Employee>>(null);
 
<DataGrid
  ref={gridRef}
  data={employees}
  columns={columns}
  getRowId="id"
  enableRowSelection={(row) => row.active} // or just enableRowSelection
  onStateChange={(next, prev) => {
    if (next.rowSelection !== prev.rowSelection) {
      console.log("Selected ids:", Object.keys(next.rowSelection));
    }
  }}
/>;
 
const selected = gridRef.current?.getSelectedRows();
  • Click a checkbox to toggle a row. Shift + click selects the range from the last clicked row.
  • The header checkbox selects every row matching the current filters (in server mode, the rows of the current page). It shows a partial state when only some are selected.
  • Selection is stored by row id, so it survives sorting, filtering and data refreshes.
  • rowSelection in state looks like { "12": true, "57": true }.

Controlled State

Every key of GridState can be controlled. Pass the keys you want to own in state and update them in onStateChange. Keys you don't pass stay internal.

tsx
const [sorting, setSorting] = useState<SortItem[]>([]);
 
<DataGrid
  data={data}
  columns={columns}
  state={{ sorting }}
  onStateChange={(next) => setSorting(next.sorting)}
/>;

Saving the column layout

tsx
const saved = JSON.parse(localStorage.getItem("employees-layout") ?? "{}");
 
<DataGrid
  data={data}
  columns={columns}
  initialState={saved}
  onStateChange={({
    columnOrder,
    columnSizing,
    columnPinning,
    columnVisibility,
    density,
  }) => {
    localStorage.setItem(
      "employees-layout",
      JSON.stringify({
        columnOrder,
        columnSizing,
        columnPinning,
        columnVisibility,
        density,
      }),
    );
  }}
/>;

GridState

KeyTypeDefaultDescription
sortingSortItem[][]Active sorts in priority order.
filtersColumnFilter[][]Active column filters.
globalFilterstring""Search text.
pagination{ pageIndex: number; pageSize: number }{ pageIndex: 0, pageSize: 50 }Zero-based page index and page size.
rowSelectionRecord<string, boolean>{}Selected row ids.
columnOrderstring[][] (definition order)Order of unpinned columns.
columnVisibilityRecord<string, boolean>from hiddenfalse hides a column.
columnSizingRecord<string, number>{}Widths set by resizing.
columnPinning{ left: string[]; right: string[] }from pinPinned column ids, in display order.
density"compact" | "standard" | "comfortable""standard"Row height: 32, 40 or 52 px.
ts
interface SortItem {
  columnId: string;
  desc: boolean;
}
 
interface ColumnFilter {
  columnId: string;
  operator: FilterOperator;
  value?: string | number | boolean | (string | number)[] | null;
  value2?: string | number | boolean | (string | number)[] | null; // upper bound for "between"
}
 
interface GridQuery {
  sorting: SortItem[];
  filters: ColumnFilter[];
  globalFilter: string;
  pagination: { pageIndex: number; pageSize: number };
}

Server Side Pagination

In server mode the grid does not sort, filter or paginate. It reports what the user asked for through onQueryChange, and you fetch the matching page. This replaces lazy, pageSettings, pageStatus, activeFilterArrayValue, onSearch and the usePaginatedData hook.

Client

tsx
"use client";
 
import { useEffect, useRef, useState } from "react";
import { DataGrid, type GridApi, type GridQuery } from "@/component-lib/data-grid";
import { columns, type Order } from "./columns";
 
const initialQuery: GridQuery = {
  sorting: [{ columnId: "orderedAt", desc: true }],
  filters: [],
  globalFilter: "",
  pagination: { pageIndex: 0, pageSize: 25 },
};
 
function toSearchParams(query: GridQuery) {
  const params = new URLSearchParams({
    page: String(query.pagination.pageIndex + 1),
    pageSize: String(query.pagination.pageSize),
  });
  if (query.globalFilter) params.set("search", query.globalFilter);
  if (query.sorting.length) params.set("sort", JSON.stringify(query.sorting));
  if (query.filters.length)
    params.set("filters", JSON.stringify(query.filters));
  return params;
}
 
const NO_ROWS: Order[] = [];
 
export default function OrdersGrid() {
  const gridRef = useRef<GridApi<Order>>(null);
  const [query, setQuery] = useState(initialQuery);
  const [result, setResult] = useState<{
    query: GridQuery;
    data: Order[];
    total: number;
  } | null>(null);
 
  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/orders?${toSearchParams(query)}`, { signal: controller.signal })
      .then((res) => res.json())
      .then((body) => setResult({ query, data: body.data, total: body.total }))
      .catch(() => {}); // aborted or failed
    return () => controller.abort(); // cancels the previous request when the query changes
  }, [query]);
 
  return (
    <DataGrid
      ref={gridRef}
      mode="server"
      data={result?.data ?? NO_ROWS}
      rowCount={result?.total ?? 0}
      loading={result?.query !== query}
      columns={columns}
      getRowId="id"
      initialState={initialQuery}
      onQueryChange={setQuery}
    />
  );
}

With TanStack Query the fetching part becomes:

tsx
const { data, isFetching } = useQuery({
  queryKey: ["orders", query],
  queryFn: ({ signal }) => fetch(`/api/orders?${toSearchParams(query)}`, { signal }).then((r) => r.json()),
  placeholderData: keepPreviousData,
});
 
<DataGrid mode="server" data={data?.data ?? NO_ROWS} rowCount={data?.total ?? 0} loading={isFetching} ... />

API contract

The grid doesn't require a specific API format; the example above uses this one.

Query parameters

ParameterTypeRequiredDescription
pagenumberNoPage number, starting at 1 (default: 1).
pageSizenumberNoRows per page (default: 25).
searchstringNoGlobal search text.
sortJSON SortItem[]Noe.g. [{"columnId":"total","desc":true}]
filtersJSON ColumnFilter[]Noe.g. [{"columnId":"status","operator":"in","value":["paid"]}]
export"true"NoReturn all matching rows, ignoring pagination.

Example URL:

bash
/api/orders?page=2&pageSize=25&search=keyboard&sort=%5B%7B%22columnId%22%3A%22total%22%2C%22desc%22%3Atrue%7D%5D

Response

json
{
  "data": [{ "id": "ORD-100001", "customer": "Ava Smith", "total": 120.5 }],
  "total": 25000
}

Error responses

  • 400 Bad Request: invalid sort or filters JSON.
  • 500 Internal Server Error: any other error.

Node.js (Express)

js
import express from "express";
import cors from "cors";
 
const app = express();
app.use(cors());
 
// Fake database. Dates are stored as "YYYY-MM-DD" strings.
const ORDERS = Array.from({ length: 500 }, (_, i) => ({
  id: `ORD-${100001 + i}`,
  customer: `Customer ${i + 1}`,
  status: ["pending", "paid", "shipped"][i % 3],
  total: Math.round(Math.random() * 50000) / 100,
  orderedAt: `2026-0${(i % 9) + 1}-1${i % 10}`,
}));
 
const isEmpty = (v) => v === null || v === undefined || v === "";
const text = (v) => String(v ?? "").toLowerCase();
 
const OPERATORS = {
  contains: (v, f) => text(v).includes(text(f.value)),
  notContains: (v, f) => !text(v).includes(text(f.value)),
  equals: (v, f) =>
    typeof f.value === "string" ? text(v) === text(f.value) : v === f.value,
  notEquals: (v, f) =>
    typeof f.value === "string" ? text(v) !== text(f.value) : v !== f.value,
  startsWith: (v, f) => text(v).startsWith(text(f.value)),
  endsWith: (v, f) => text(v).endsWith(text(f.value)),
  gt: (v, f) => v > f.value,
  gte: (v, f) => v >= f.value,
  lt: (v, f) => v < f.value,
  lte: (v, f) => v <= f.value,
  between: (v, f) =>
    (isEmpty(f.value) || v >= f.value) && (isEmpty(f.value2) || v <= f.value2),
  before: (v, f) => String(v) < f.value,
  after: (v, f) => String(v) > f.value,
  in: (v, f) => Array.isArray(f.value) && f.value.includes(v),
  isEmpty: (v) => isEmpty(v),
  isNotEmpty: (v) => !isEmpty(v),
};
 
function compare(a, b, sorting) {
  for (const { columnId, desc } of sorting) {
    const x = a[columnId];
    const y = b[columnId];
    if (x === y) continue;
    if (isEmpty(x)) return 1; // empty values last
    if (isEmpty(y)) return -1;
    const result =
      typeof x === "string"
        ? x.localeCompare(y, undefined, { numeric: true, sensitivity: "base" })
        : x < y
          ? -1
          : 1;
    if (result !== 0) return desc ? -result : result;
  }
  return 0;
}
 
app.get("/api/orders", (req, res) => {
  const page = Math.max(1, parseInt(req.query.page ?? "1", 10));
  const pageSize = Math.min(
    500,
    Math.max(1, parseInt(req.query.pageSize ?? "25", 10)),
  );
  const terms = text(req.query.search).split(/\s+/).filter(Boolean);
 
  let sorting;
  let filters;
  try {
    sorting = JSON.parse(req.query.sort ?? "[]");
    filters = JSON.parse(req.query.filters ?? "[]");
  } catch {
    return res.status(400).json({ error: "Invalid sort or filters" });
  }
 
  let rows = ORDERS.filter((row) =>
    filters.every((f) => {
      const operator = OPERATORS[f.operator];
      return operator ? operator(row[f.columnId], f) : true;
    }),
  );
 
  if (terms.length) {
    rows = rows.filter((row) => {
      const haystack = Object.values(row).map(text).join(" ");
      return terms.every((term) => haystack.includes(term));
    });
  }
 
  rows.sort((a, b) => compare(a, b, sorting));
 
  const data =
    req.query.export === "true"
      ? rows
      : rows.slice((page - 1) * pageSize, page * pageSize);
  res.json({ data, total: rows.length });
});
 
app.listen(5000, () => console.log("Server running on http://localhost:5000"));

Next.js Route Handler (same logic as the grid)

If the API lives in the same Next.js app, it can import the grid's framework-free functions, so the server sorts, filters and searches exactly like client mode.

ts
// app/api/orders/route.ts
import {
  buildRows,
  createFormatters,
  createRowIdGetter,
  filterRows,
  paginate,
  resolveColumns,
  sortRows,
} from "@/component-lib/data-grid/core";
import { orderColumns, type Order } from "@/lib/order-columns"; // column defs without "use client"
import { getOrders } from "@/lib/db";
 
const columns = resolveColumns(orderColumns);
const formatters = createFormatters("en-US");
 
export async function GET(request: Request) {
  const params = new URL(request.url).searchParams;
  const rows = buildRows(await getOrders(), createRowIdGetter<Order>("id"));
 
  const filtered = filterRows(
    rows,
    columns,
    JSON.parse(params.get("filters") ?? "[]"),
    params.get("search") ?? "",
    formatters,
  );
  const sorted = sortRows(
    filtered,
    columns,
    JSON.parse(params.get("sort") ?? "[]"),
  );
  const page = paginate(
    sorted,
    {
      pageIndex: Number(params.get("page") ?? 1) - 1,
      pageSize: Number(params.get("pageSize") ?? 25),
    },
    { enabled: params.get("export") !== "true", server: false },
  );
 
  return Response.json({
    data: page.rows.map((row) => row.original),
    total: sorted.length,
  });
}

For large tables, apply the same query in your database instead of loading every row.

Exports in server mode

The grid only holds the current page, so the Export menu exports that page. To export every matching row, fetch them and pass them to the API:

tsx
const exportAll = async (format: "excel" | "csv") => {
  const params = toSearchParams(query);
  params.set("export", "true");
  const { data } = await fetch(`/api/orders?${params}`).then((r) => r.json());
 
  if (format === "excel")
    await gridRef.current?.exportExcel({ rows: data, fileName: "orders" });
  else await gridRef.current?.exportCsv({ rows: data, fileName: "orders" });
};
 
<DataGrid
  mode="server"
  toolbar={{
    export: false,
    end: <button onClick={() => exportAll("excel")}>Export all</button>,
  }}
  {...otherProps}
/>;