Server-Side Data
Integrate the React grid with server-side sorting, filtering, and windowed row loading by implementing a remote data source that streams chunks on-demand efficiently.
For very large datasets or when data must remain on the server, use server-side data operations.
When to Use Server-Side
- Dataset too large to load into browser memory
- Data requires real-time updates from server
- Complex filtering/sorting logic on server
- Security requirements to keep data server-side
Basic Setup
import { Grid, createServerDataSource } from "@gp-grid/react";
const dataSource = createServerDataSource(async (request) => {
const response = await fetch("/api/grid-data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
return response.json();
});
function ServerGrid() {
return (
<Grid
columns={columns}
dataSource={dataSource}
rowHeight={36}
/>
);
}Request Format
The server receives a request describing the absolute row range, plus sort/filter state. endRow is exclusive, so endRow - startRow is the number of rows to return.
interface DataSourceRequest {
range: {
startRow: number; // First row index to fetch (0-indexed, inclusive)
endRow: number; // First row index after the range (exclusive)
};
sort?: SortModel[]; // Sort configuration
filter?: FilterModel; // Filter configuration
}
// Example request — the grid asks for rows 0..99 sorted by name asc
{
range: { startRow: 0, endRow: 100 },
sort: [{ colId: "name", direction: "asc" }],
filter: {
salary: {
groups: [{
conditions: [{ type: "number", operator: ">", value: 50000 }],
combination: "and"
}],
combination: "and"
}
}
}Response Format
Return rows and total count:
interface DataSourceResponse<TData> {
rows: TData[]; // Current page of data
totalRows: number; // Total rows matching filters
}
// Example response
{
rows: [
{ id: 1, name: "Giovanni", salary: 75000 },
{ id: 2, name: "Luca", salary: 82000 },
// ...
],
totalRows: 10000
}Express.js Example
// server.ts
import express from "express";
app.post("/api/grid-data", async (req, res) => {
const { range, sort, filter } = req.body;
// Build database query
let query = db.select().from(employees);
// Apply filters
if (filter) {
for (const [field, model] of Object.entries(filter)) {
// Translate each condition to a database predicate, join conditions
// with group.combination, then join groups with model.combination.
query = query.where(buildColumnPredicate(field, model));
}
}
// Apply sorting
if (sort?.length) {
for (const s of sort) {
query = query.orderBy(s.colId, s.direction);
}
}
// Get total count
const totalRows = await query.clone().count();
// Apply range (endRow is exclusive)
const { startRow, endRow } = range;
query = query.offset(startRow).limit(endRow - startRow);
const rows = await query;
res.json({ rows, totalRows });
});buildColumnPredicate is application-specific because query builders expose
different APIs. It must preserve both Boolean levels: group.combination
inside each group and model.combination between groups.
Handling Sort Models
// SortModel structure
type SortModel = {
colId: string;
direction: "asc" | "desc";
};
// Multiple columns (for multi-sort)
[
{ colId: "department", direction: "asc" },
{ colId: "salary", direction: "desc" }
]Handling Filter Models
// FilterModel structure
type FilterModel = Record<string, ColumnFilterModel>;
interface ColumnFilterModel {
groups: FilterConditionGroup[];
combination: "and" | "or"; // joins groups
}
interface FilterConditionGroup {
conditions: FilterCondition[];
combination: "and" | "or"; // joins conditions in this group
}
// Example: salary > 50000 AND salary < 100000
const salaryFilter: FilterModel = {
salary: {
groups: [{
conditions: [
{ type: "number", operator: ">", value: 50000 },
{ type: "number", operator: "<", value: 100000 },
],
combination: "and",
}],
combination: "and",
},
};
// Example: (starts with A AND ends with z) OR equals Giovanni
const nameFilter: FilterModel = {
name: {
groups: [
{
conditions: [
{ type: "text", operator: "startsWith", value: "A" },
{ type: "text", operator: "endsWith", value: "z" },
],
combination: "and",
},
{
conditions: [
{ type: "text", operator: "equals", value: "Giovanni" },
],
combination: "and",
},
],
combination: "or",
},
};
// Example: values (checkbox) filter — selectedValues holds RAW cell values
const statusFilter: FilterModel = {
status: {
groups: [{
conditions: [{
type: "text",
operator: "equals",
selectedValues: new Set([0, 1]), // raw values, never display labels
includeBlank: false, // true when the "(Blanks)" entry is ticked
}],
combination: "and",
}],
combination: "and",
},
};ColumnFilterModel.combination joins groups, while each group's
combination joins its conditions. Filters for separate columns are always
combined with AND.
The legacy flat shape is still accepted by GridCore.setFilter() and
normalized without changing its left-to-right result. getFilterModel() and
DataSourceRequest.filter always expose the canonical grouped shape.
selectedValues always contains raw cell values: a valueFormatter on the column changes only what the popup displays, never what your server receives. Match them directly against your database column (e.g. WHERE status IN (...)), and treat includeBlank: true as "also include NULL/empty rows".
selectedValues is a Set, and JSON.stringify serializes a Set as {}. Convert it before sending the request over the wire:
const serializeFilter = (filter: FilterModel) =>
Object.fromEntries(
Object.entries(filter).map(([field, model]) => [
field,
{
...model,
groups: model.groups.map((group) => ({
...group,
conditions: group.conditions.map((condition) =>
"selectedValues" in condition && condition.selectedValues
? {
...condition,
selectedValues: [...condition.selectedValues],
}
: condition,
),
})),
},
]),
);Error Handling
const dataSource = createServerDataSource(async (request) => {
try {
const response = await fetch("/api/grid-data", {
method: "POST",
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
console.error("Failed to fetch grid data:", error);
// Return empty result on error
return { rows: [], totalRows: 0 };
}
});Caching Considerations
For better UX, consider caching pages:
const cache = new Map<string, DataSourceResponse>();
const dataSource = createServerDataSource(async (request) => {
const cacheKey = JSON.stringify(request);
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!;
}
const result = await fetchFromServer(request);
cache.set(cacheKey, result);
return result;
});Clear cache when data changes on the server.
Styling
Customize the appearance of the React grid with CSS variables, theme tokens, and class overrides to match your application's design system seamlessly.
FAQ
Browse frequently asked questions about using gp-grid with React, covering common pitfalls, integration tips, and answers gathered from the community.