Server-side data loading with pagination and sorting.
Server mode delegates sorting, filtering, and pagination to the server. Only the requested page of data is loaded, enabling efficient handling of large datasets with millions of rows.
<ActiveGrid
mode="server"
fetchFn={async (params) => {
const response = await api.getData(params);
return {
data: response.data,
totalCount: response.total,
};
}}
columns={columns}
{...}
/>interface ServerFetchParams {
pageIndex: number; // Current page (0-based)
pageSize: number; // Rows per page
sorting: SortingState; // Sort configuration
filters: ColumnFiltersState; // Column filters
globalFilter?: string; // Global search query
}
interface PagedResponse<TData> {
data: TData[]; // Current page data
totalCount: number; // Total row count (required)
meta?: {
aggregations?: Record<string, number>;
facets?: Record<string, FacetOption[]>;
};
}
type ServerFetchFunction<TData> = (
params: ServerFetchParams
) => Promise<PagedResponse<TData>>;<ActiveGrid
mode="server"
fetchFn={async (params) => {
const response = await fetch(`/api/users?${new URLSearchParams({
page: params.pageIndex.toString(),
limit: params.pageSize.toString(),
})}`);
const result = await response.json();
return {
data: result.users,
totalCount: result.total,
};
}}
columns={columns}
pagination={{
enabled: true,
pageSizeOptions: [10, 25, 50, 100],
}}
initialPageSize={25}
{...}
/>fetchFn={async (params) => {
const sortBy = params.sorting[0]?.id;
const sortDir = params.sorting[0]?.desc ? 'desc' : 'asc';
const response = await api.getUsers({
page: params.pageIndex,
limit: params.pageSize,
sortBy,
sortDir,
});
return {
data: response.data,
totalCount: response.total,
};
}}fetchFn={async (params) => {
// Convert filters to API format
const filters = params.filters.reduce((acc, filter) => {
acc[filter.id] = filter.value;
return acc;
}, {} as Record<string, any>);
const response = await api.getUsers({
page: params.pageIndex,
limit: params.pageSize,
filters,
search: params.globalFilter,
});
return {
data: response.data,
totalCount: response.total,
};
}}fetchFn={async (params) => {
// Build query parameters
const queryParams = {
page: params.pageIndex + 1, // Convert to 1-based if needed
limit: params.pageSize,
// Sorting
sortBy: params.sorting[0]?.id,
sortDir: params.sorting[0]?.desc ? 'desc' : 'asc',
// Global search
search: params.globalFilter,
// Column filters
...params.filters.reduce((acc, filter) => {
acc[`filter_${filter.id}`] = filter.value;
return acc;
}, {} as Record<string, any>),
};
const response = await fetch(
`/api/users?${new URLSearchParams(queryParams)}`
);
const result = await response.json();
return {
data: result.users,
totalCount: result.total,
};
}}New in v5.1
Return server-side aggregations and facet counts:
fetchFn={async (params) => {
const response = await api.getOrders(params);
return {
data: response.orders,
totalCount: response.total,
meta: {
// Aggregations for footer display
aggregations: {
amount: response.totalAmount,
quantity: response.totalQuantity,
},
// Facets for filter options with counts
facets: {
status: [
{ label: 'Pending', value: 'pending', count: 45 },
{ label: 'Shipped', value: 'shipped', count: 32 },
{ label: 'Delivered', value: 'delivered', count: 128 },
],
category: [
{ label: 'Electronics', value: 'electronics', count: 67 },
{ label: 'Clothing', value: 'clothing', count: 89 },
],
},
},
};
}}The grid automatically shows loading states during data fetching:
<ActiveGrid
mode="server"
fetchFn={fetchData}
columns={columns}
// Loading indicator shown automatically
{...}
/>Handle fetch errors gracefully:
fetchFn={async (params) => {
try {
const response = await api.getUsers(params);
return {
data: response.data,
totalCount: response.total,
};
} catch (error) {
console.error('Failed to fetch data:', error);
toast.error('Failed to load data');
// Return empty result
return {
data: [],
totalCount: 0,
};
}
}}Access the table instance to refetch data:
Handle filter operators from the server:
Global search is automatically debounced (300ms default):
Maintain selection across page changes:
For server-side selection across pages, you must provide getRowId to ensure consistent row identification.
Server mode handles datasets of any size:
import { useActiveGrid } from '@workspace/active-grid';
function MyComponent() {
const table = useActiveGrid();
const handleRefresh = () => {
table.refetch();
};
return (
<Button onClick={handleRefresh}>
Refresh Data
</Button>
);
}import { useQuery } from '@tanstack/react-query';
function UsersTable() {
const [serverParams, setServerParams] = useState<ServerFetchParams>({
pageIndex: 0,
pageSize: 25,
sorting: [],
filters: [],
});
const { data, isLoading } = useQuery({
queryKey: ['users', serverParams],
queryFn: () => api.getUsers(serverParams),
});
return (
<ActiveGrid
mode="server"
fetchFn={async (params) => {
setServerParams(params);
return data || { data: [], totalCount: 0 };
}}
columns={columns}
{...}
/>
);
}fetchFn={async (params) => {
const filters = params.filters.map(filter => {
const value = filter.value as FilterValue;
// Handle operator-based filters
if (typeof value === 'object' && value.operator) {
return {
field: filter.id,
operator: value.operator,
value: value.value,
valueTo: value.valueTo,
};
}
// Simple value
return {
field: filter.id,
operator: 'equals',
value: value,
};
});
const response = await api.getUsers({
page: params.pageIndex,
limit: params.pageSize,
filters,
});
return {
data: response.data,
totalCount: response.total,
};
}}fetchFn={async (params) => {
// Handle multiple sort columns
const sorting = params.sorting.map(sort => ({
field: sort.id,
direction: sort.desc ? 'desc' : 'asc',
}));
const response = await api.getUsers({
page: params.pageIndex,
limit: params.pageSize,
sorting,
});
return {
data: response.data,
totalCount: response.total,
};
}}<ActiveGrid
mode="server"
fetchFn={fetchData}
toolbar={{
search: {
placeholder: 'Search...',
debounceMs: 500, // Custom debounce
},
}}
{...}
/>import { useState } from 'react';
import { RowSelectionState } from '@tanstack/react-table';
function UsersTable() {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
return (
<ActiveGrid
mode="server"
fetchFn={fetchData}
columns={columns}
state={{ rowSelection }}
onRowSelectionChange={setRowSelection}
getRowId={(row) => row.id} // Required for persistent selection
{...}
/>
);
}import { useState } from 'react';
import { ActiveGrid, ServerFetchParams, PagedResponse } from '@workspace/active-grid';
type User = {
id: string;
name: string;
email: string;
status: string;
createdAt: string;
};
function UsersTable() {
const fetchUsers = async (
params: ServerFetchParams
): Promise<PagedResponse<User>> => {
try {
// Build request
const request = {
page: params.pageIndex + 1,
limit: params.pageSize,
sortBy: params.sorting[0]?.id,
sortDir: params.sorting[0]?.desc ? 'desc' : 'asc',
search: params.globalFilter,
filters: params.filters.reduce((acc, filter) => {
acc[filter.id] = filter.value;
return acc;
}, {} as Record<string, any>),
};
// Fetch data
const response = await fetch(
`/api/users?${new URLSearchParams(request as any)}`
);
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const result = await response.json();
return {
data: result.users,
totalCount: result.total,
meta: {
aggregations: {
totalRevenue: result.totalRevenue,
},
facets: {
status: result.statusCounts,
},
},
};
} catch (error) {
console.error('Error fetching users:', error);
toast.error('Failed to load users');
return {
data: [],
totalCount: 0,
};
}
};
const columns: GridColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
meta: {
filterType: 'text',
},
},
{
accessorKey: 'email',
header: 'Email',
meta: {
filterType: 'text',
},
},
{
accessorKey: 'status',
header: 'Status',
meta: {
filterType: 'select',
// Options populated from facets
},
},
{
accessorKey: 'createdAt',
header: 'Created',
meta: {
filterType: 'date',
valueFormatter: (value) => new Date(value).toLocaleDateString(),
},
},
];
return (
<ActiveGrid
mode="server"
fetchFn={fetchUsers}
columns={columns}
getRowId={(row) => row.id}
pagination={{
enabled: true,
pageSizeOptions: [10, 25, 50, 100],
}}
initialPageSize={25}
toolbar={{
search: {
placeholder: 'Search users...',
debounceMs: 400,
},
}}
/>
);
}app.get('/api/users', async (req, res) => {
const {
page = 1,
limit = 25,
sortBy = 'createdAt',
sortDir = 'desc',
search,
filter_status,
} = req.query;
let query = db('users');
// Global search
if (search) {
query = query.where(function() {
this.where('name', 'like', `%${search}%`)
.orWhere('email', 'like', `%${search}%`);
});
}
// Column filters
if (filter_status) {
query = query.where('status', filter_status);
}
// Get total count
const [{ count }] = await query.clone().count('* as count');
// Apply sorting and pagination
const users = await query
.orderBy(sortBy, sortDir)
.limit(limit)
.offset((page - 1) * limit);
res.json({
users,
total: count,
});
});import { ServerFetchParams, PagedResponse } from '@workspace/active-grid';
type User = {
id: string;
name: string;
};
const fetchUsers = async (
params: ServerFetchParams
): Promise<PagedResponse<User>> => {
// Implementation
};
<ActiveGrid<User>
mode="server"
fetchFn={fetchUsers}
columns={columns}
{...}
/>