@gp-grid/react ๐Ÿ ๐ŸŽ๏ธ

May 24, 2026 ยท View on GitHub

gp-grid Logo

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.

Table of Contents

Features

  • 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, Ctrl+V
  • Custom Renderers: Registry-based cell, edit, and header renderers
  • Dark Mode: Built-in dark theme support
  • TypeScript: Full type safety with exported types

Installation

Use npm, yarn or pnpm

pnpm add @gp-grid/react

Quick Start

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 (
    <div style={{ width: "800px", height: "400px" }}>
      <Grid columns={columns} rowData={data} rowHeight={36} />
    </div>
  );
}

Examples

Client-Side Data Source with Sorting and Filtering

For larger datasets with client-side sort/filter operations:

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 (
    <div style={{ width: "100%", height: "500px" }}>
      <Grid
        columns={columns}
        dataSource={dataSource}
        rowHeight={36}
        headerHeight={40}
        showFilters={true}
        filterDebounce={300}
      />
    </div>
  );
}

Server-Side Data Source

For datasets too large to load entirely in memory, use a server-side data source:

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<DataSourceResponse<User>> {
  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<User>(fetchUsers),
    [],
  );

  return (
    <div style={{ width: "100%", height: "600px" }}>
      <Grid
        columns={columns}
        dataSource={dataSource}
        rowHeight={36}
        headerHeight={40}
        showFilters={true}
        filterDebounce={500} // Debounce filter requests
        darkMode={true}
      />
    </div>
  );
}

Custom Cell Renderers

Use the registry pattern to define reusable renderers:

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 (
      <span style={{ color: "#047857", fontWeight: 600 }}>
        ${value.toLocaleString("en-US", { minimumFractionDigits: 2 })}
      </span>
    );
  },

  // Status badge
  statusBadge: (params: CellRendererParams) => {
    const status = params.value as Order["status"];
    const colors: Record<string, { bg: string; text: string }> = {
      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 (
      <span
        style={{
          backgroundColor: color.bg,
          color: color.text,
          padding: "2px 8px",
          borderRadius: "12px",
          fontSize: "12px",
          fontWeight: 600,
        }}
      >
        {status.toUpperCase()}
      </span>
    );
  },

  // Bold text
  bold: (params: CellRendererParams) => (
    <strong>{String(params.value ?? "")}</strong>
  ),
};

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 (
    <div style={{ width: "100%", height: "400px" }}>
      <Grid
        columns={columns}
        rowData={orders}
        rowHeight={40}
        cellRenderers={cellRenderers}
      />
    </div>
  );
}

Editable Cells with Custom Editors

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 (
      <select
        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",
        }}
      >
        <option value="low">Low</option>
        <option value="medium">Medium</option>
        <option value="high">High</option>
      </select>
    );
  },

  checkbox: (params: EditRendererParams) => (
    <input
      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 (
    <div style={{ width: "600px", height: "300px" }}>
      <Grid
        columns={columns}
        dataSource={dataSource}
        rowHeight={40}
        editRenderers={editRenderers}
      />
    </div>
  );
}

Dark Mode

<Grid columns={columns} rowData={data} rowHeight={36} darkMode={true} />

API Reference

GridProps

PropTypeDefaultDescription
columnsColumnDefinition[]requiredColumn definitions
dataSourceDataSource<TData>-Data source for fetching data
rowDataTData[]-Alternative: raw data array (wrapped in client data source)
rowHeightnumberrequiredHeight of each row in pixels
headerHeightnumberrowHeightHeight of header row
overscannumber3Number of rows to render outside viewport
showFiltersbooleanfalseShow filter row below headers
filterDebouncenumber300Debounce time for filter input (ms)
darkModebooleanfalseEnable dark theme
cellRenderersRecord<string, ReactCellRenderer>{}Cell renderer registry
editRenderersRecord<string, ReactEditRenderer>{}Edit renderer registry
headerRenderersRecord<string, ReactHeaderRenderer>{}Header renderer registry
cellRendererReactCellRenderer-Global fallback cell renderer
editRendererReactEditRenderer-Global fallback edit renderer
headerRendererReactHeaderRenderer-Global fallback header renderer

ColumnDefinition

PropertyTypeDescription
fieldstringProperty path in row data (supports dot notation: "address.city")
colIdstringUnique column ID (defaults to field)
cellDataTypeCellDataType"text" | "number" | "boolean" | "date" | "object"
widthnumberColumn width in pixels
headerNamestringDisplay name in header (defaults to field)
editablebooleanEnable cell editing
cellRendererstringKey in cellRenderers registry
editRendererstringKey in editRenderers registry
headerRendererstringKey in headerRenderers registry

Renderer Types

// 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

KeyAction
Arrow keysNavigate between cells
Shift + ArrowExtend selection
EnterStart editing / Commit edit
EscapeCancel edit / Clear selection
TabCommit and move right
Shift + TabCommit and move left
F2Start editing
Delete / BackspaceStart editing with empty value
Ctrl + ASelect all
Ctrl + CCopy selection to clipboard
Ctrl + VPaste clipboard values into selection
Any characterStart 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 element

Donations

Keeping 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! ๐Ÿป

Support the project