A high-performance React data grid component with virtual scrolling, cell selection, sorting, filtering, and Excel-like editing
npm install @gp-grid/react
A high-performance, feature lean React data grid component built to manage grids with huge amount (millions) of rows. It's based on its core dependency: @gp-grid/core, featuring virtual scrolling, cell selection, sorting, filtering, editing, and Excel-like fill handle.
- Features
- Installation
- Quick Start
- Examples
- API Reference
- Keyboard Shortcuts
- Styling
- Donations
- Virtual Scrolling: Efficiently handles millions of rows through slot-based recycling
- Cell Selection: Single cell, range selection, Shift+click extend, Ctrl+click toggle
- Multi-Column Sorting: Click to sort, Shift+click for multi-column sort
- Column Filtering: Built-in filter row with debounced input
- Cell Editing: Double-click or press Enter to edit, with custom editor support
- Fill Handle: Excel-like drag-to-fill for editable cells
- Keyboard Navigation: Arrow keys, Tab, Enter, Escape, Ctrl+A, Ctrl+C
- Custom Renderers: Registry-based cell, edit, and header renderers
- Dark Mode: Built-in dark theme support
- TypeScript: Full type safety with exported types
Use npm, yarn or pnpm
``bash`
pnpm add @gp-grid/react
`tsx
import { Grid, type ColumnDefinition } from "@gp-grid/react";
interface Person {
id: number;
name: string;
age: number;
email: string;
}
const columns: ColumnDefinition[] = [
{ field: "id", cellDataType: "number", width: 80, headerName: "ID" },
{ field: "name", cellDataType: "text", width: 150, headerName: "Name" },
{ field: "age", cellDataType: "number", width: 80, headerName: "Age" },
{ field: "email", cellDataType: "text", width: 250, headerName: "Email" },
];
const data: Person[] = [
{ id: 1, name: "Alice", age: 30, email: "alice@example.com" },
{ id: 2, name: "Bob", age: 25, email: "bob@example.com" },
{ id: 3, name: "Charlie", age: 35, email: "charlie@example.com" },
];
function App() {
return (
Examples
$3
For larger datasets with client-side sort/filter operations:
`tsx
import { useMemo } from "react";
import {
Grid,
createClientDataSource,
type ColumnDefinition,
} from "@gp-grid/react";interface Product {
id: number;
name: string;
price: number;
category: string;
}
const columns: ColumnDefinition[] = [
{ field: "id", cellDataType: "number", width: 80, headerName: "ID" },
{ field: "name", cellDataType: "text", width: 200, headerName: "Product" },
{ field: "price", cellDataType: "number", width: 100, headerName: "Price" },
{
field: "category",
cellDataType: "text",
width: 150,
headerName: "Category",
},
];
function ProductGrid() {
const products: Product[] = useMemo(
() =>
Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name:
Product ${i + 1},
price: Math.round(Math.random() * 1000) / 10,
category: ["Electronics", "Clothing", "Food", "Books"][i % 4],
})),
[],
); const dataSource = useMemo(
() => createClientDataSource(products),
[products],
);
return (
columns={columns}
dataSource={dataSource}
rowHeight={36}
headerHeight={40}
showFilters={true}
filterDebounce={300}
/>
);
}
`$3
For datasets too large to load entirely in memory, use a server-side data source:
`tsx
import { useMemo } from "react";
import {
Grid,
createServerDataSource,
type ColumnDefinition,
type DataSourceRequest,
type DataSourceResponse,
} from "@gp-grid/react";interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: string;
}
const columns: ColumnDefinition[] = [
{ field: "id", cellDataType: "number", width: 80, headerName: "ID" },
{ field: "name", cellDataType: "text", width: 150, headerName: "Name" },
{ field: "email", cellDataType: "text", width: 250, headerName: "Email" },
{ field: "role", cellDataType: "text", width: 120, headerName: "Role" },
{
field: "createdAt",
cellDataType: "dateString",
width: 150,
headerName: "Created",
},
];
// API fetch function that handles pagination, sorting, and filtering
async function fetchUsers(
request: DataSourceRequest,
): Promise> {
const { pagination, sort, filter } = request;
// Build query parameters
const params = new URLSearchParams({
page: String(pagination.pageIndex),
limit: String(pagination.pageSize),
});
// Add sorting parameters
if (sort && sort.length > 0) {
// Format: sortBy=name:asc,email:desc
const sortString = sort.map((s) =>
${s.colId}:${s.direction}).join(",");
params.set("sortBy", sortString);
} // Add filter parameters
if (filter) {
Object.entries(filter).forEach(([field, value]) => {
if (value) {
params.set(
filter[${field}], value);
}
});
} // Make API request
const response = await fetch(
https://api.example.com/users?${params}); if (!response.ok) {
throw new Error(
API error: ${response.status});
} const data = await response.json();
// Return in DataSourceResponse format
return {
rows: data.users, // Array of User objects
totalRows: data.total, // Total count for virtual scrolling
};
}
function UserGrid() {
// Create server data source - memoize to prevent recreation
const dataSource = useMemo(
() => createServerDataSource(fetchUsers),
[],
);
return (
columns={columns}
dataSource={dataSource}
rowHeight={36}
headerHeight={40}
showFilters={true}
filterDebounce={500} // Debounce filter requests
darkMode={true}
/>
);
}
`$3
Use the registry pattern to define reusable renderers:
`tsx
import {
Grid,
type ColumnDefinition,
type CellRendererParams,
} from "@gp-grid/react";interface Order {
id: number;
customer: string;
total: number;
status: "pending" | "shipped" | "delivered" | "cancelled";
}
// Define reusable renderers
const cellRenderers = {
// Currency formatter
currency: (params: CellRendererParams) => {
const value = params.value as number;
return (
${value.toLocaleString("en-US", { minimumFractionDigits: 2 })}
);
},
// Status badge
statusBadge: (params: CellRendererParams) => {
const status = params.value as Order["status"];
const colors: Record = {
pending: { bg: "#fef3c7", text: "#92400e" },
shipped: { bg: "#dbeafe", text: "#1e40af" },
delivered: { bg: "#dcfce7", text: "#166534" },
cancelled: { bg: "#fee2e2", text: "#991b1b" },
};
const color = colors[status] ?? { bg: "#f3f4f6", text: "#374151" };
return (
style={{
backgroundColor: color.bg,
color: color.text,
padding: "2px 8px",
borderRadius: "12px",
fontSize: "12px",
fontWeight: 600,
}}
>
{status.toUpperCase()}
);
},
// Bold text
bold: (params: CellRendererParams) => (
{String(params.value ?? "")}
),
};
const columns: ColumnDefinition[] = [
{
field: "id",
cellDataType: "number",
width: 80,
headerName: "ID",
cellRenderer: "bold",
},
{
field: "customer",
cellDataType: "text",
width: 200,
headerName: "Customer",
},
{
field: "total",
cellDataType: "number",
width: 120,
headerName: "Total",
cellRenderer: "currency",
},
{
field: "status",
cellDataType: "text",
width: 120,
headerName: "Status",
cellRenderer: "statusBadge",
},
];
function OrderGrid({ orders }: { orders: Order[] }) {
return (
columns={columns}
rowData={orders}
rowHeight={40}
cellRenderers={cellRenderers}
/>
);
}
`$3
`tsx
import { useState } from "react";
import {
Grid,
createClientDataSource,
type ColumnDefinition,
type EditRendererParams,
} from "@gp-grid/react";interface Task {
id: number;
title: string;
priority: "low" | "medium" | "high";
completed: boolean;
}
// Custom select editor for priority field
const editRenderers = {
prioritySelect: (params: EditRendererParams) => {
const [value, setValue] = useState(params.initialValue as string);
return (
autoFocus
value={value}
onChange={(e) => {
setValue(e.target.value);
params.onValueChange(e.target.value);
}}
onBlur={() => params.onCommit()}
onKeyDown={(e) => {
if (e.key === "Enter") params.onCommit();
if (e.key === "Escape") params.onCancel();
}}
style={{
width: "100%",
height: "100%",
border: "none",
outline: "none",
padding: "0 8px",
}}
>
);
},
checkbox: (params: EditRendererParams) => (
type="checkbox"
autoFocus
defaultChecked={params.initialValue as boolean}
onChange={(e) => {
params.onValueChange(e.target.checked);
params.onCommit();
}}
style={{ width: 20, height: 20 }}
/>
),
};
const columns: ColumnDefinition[] = [
{ field: "id", cellDataType: "number", width: 60, headerName: "ID" },
{
field: "title",
cellDataType: "text",
width: 300,
headerName: "Title",
editable: true, // Uses default text input
},
{
field: "priority",
cellDataType: "text",
width: 120,
headerName: "Priority",
editable: true,
editRenderer: "prioritySelect", // Custom editor
},
{
field: "completed",
cellDataType: "boolean",
width: 100,
headerName: "Done",
editable: true,
editRenderer: "checkbox", // Custom editor
},
];
function TaskGrid() {
const tasks: Task[] = [
{ id: 1, title: "Write documentation", priority: "high", completed: false },
{ id: 2, title: "Fix bugs", priority: "medium", completed: true },
{ id: 3, title: "Add tests", priority: "low", completed: false },
];
const dataSource = createClientDataSource(tasks);
return (
columns={columns}
dataSource={dataSource}
rowHeight={40}
editRenderers={editRenderers}
/>
);
}
`$3
`tsx
`API Reference
$3
| Prop | Type | Default | Description |
| ----------------- | ------------------------------------- | ----------- | ----------------------------------------------------------- |
|
columns | ColumnDefinition[] | required | Column definitions |
| dataSource | DataSource | - | Data source for fetching data |
| rowData | TData[] | - | Alternative: raw data array (wrapped in client data source) |
| rowHeight | number | required | Height of each row in pixels |
| headerHeight | number | rowHeight | Height of header row |
| overscan | number | 3 | Number of rows to render outside viewport |
| showFilters | boolean | false | Show filter row below headers |
| filterDebounce | number | 300 | Debounce time for filter input (ms) |
| darkMode | boolean | false | Enable dark theme |
| cellRenderers | Record | {} | Cell renderer registry |
| editRenderers | Record | {} | Edit renderer registry |
| headerRenderers | Record | {} | Header renderer registry |
| cellRenderer | ReactCellRenderer | - | Global fallback cell renderer |
| editRenderer | ReactEditRenderer | - | Global fallback edit renderer |
| headerRenderer | ReactHeaderRenderer | - | Global fallback header renderer |$3
| Property | Type | Description |
| ---------------- | -------------- | ------------------------------------------------------------------- |
|
field | string | Property path in row data (supports dot notation: "address.city") |
| colId | string | Unique column ID (defaults to field) |
| cellDataType | CellDataType | "text" \| "number" \| "boolean" \| "date" \| "object" |
| width | number | Column width in pixels |
| headerName | string | Display name in header (defaults to field) |
| editable | boolean | Enable cell editing |
| cellRenderer | string | Key in cellRenderers registry |
| editRenderer | string | Key in editRenderers registry |
| headerRenderer | string | Key in headerRenderers registry |$3
`typescript
// Cell renderer receives these params
interface CellRendererParams {
value: CellValue; // Current cell value
rowData: Row; // Full row data
column: ColumnDefinition; // Column definition
rowIndex: number; // Row index
colIndex: number; // Column index
isActive: boolean; // Is this the active cell?
isSelected: boolean; // Is this cell in selection?
isEditing: boolean; // Is this cell being edited?
}// Edit renderer receives additional callbacks
interface EditRendererParams extends CellRendererParams {
initialValue: CellValue;
onValueChange: (newValue: CellValue) => void;
onCommit: () => void;
onCancel: () => void;
}
// Header renderer params
interface HeaderRendererParams {
column: ColumnDefinition;
colIndex: number;
sortDirection?: "asc" | "desc";
sortIndex?: number; // For multi-column sort
onSort: (direction: "asc" | "desc" | null, addToExisting: boolean) => void;
}
`Keyboard Shortcuts
| Key | Action |
| ------------------ | --------------------------------- |
| Arrow keys | Navigate between cells |
| Shift + Arrow | Extend selection |
| Enter | Start editing / Commit edit |
| Escape | Cancel edit / Clear selection |
| Tab | Commit and move right |
| Shift + Tab | Commit and move left |
| F2 | Start editing |
| Delete / Backspace | Start editing with empty value |
| Ctrl + A | Select all |
| Ctrl + C | Copy selection to clipboard |
| Any character | Start editing with that character |
Styling
The grid injects its own styles automatically. The main container uses these CSS classes:
-
.gp-grid-container - Main container
- .gp-grid-container--dark - Dark mode modifier
- .gp-grid-header - Header row container
- .gp-grid-header-cell - Individual header cell
- .gp-grid-row - Row container
- .gp-grid-cell - Cell container
- .gp-grid-cell--active - Active cell
- .gp-grid-cell--selected - Selected cell
- .gp-grid-cell--editing - Cell in edit mode
- .gp-grid-filter-row - Filter row container
- .gp-grid-filter-input - Filter input field
- .gp-grid-fill-handle` - Fill handle elementKeeping this library requires effort and passion, I'm a full time engineer employed on other project and I'm trying my best to keep this work free! For all the features.
If you think this project helped you achieve your goals, it's hopefully worth a beer! 🍻

https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY

bitcoin:bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m

lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhhx6rpvanhjetdvfjhyvf4xs0xu5p7