> For the complete documentation index, see [llms.txt](https://beamline.gitbook.io/beamline-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://beamline.gitbook.io/beamline-docs/guides/build-a-data-grid-and-crm-screen-in-react.md).

# Build a data grid and CRM screen in React

A virtualised React data grid for thousands of rows (sorting, filters, pinning, inline edit, range selection, footer totals, full keyboard) and a CRM pipeline screen built on it.

> The original of this page is on beamline.io: [beamline.io/guides/react-data-grid-crm](https://beamline.io/guides/react-data-grid-crm).

By Danylo Pravda, 2026-10-11

Any product with records (deals, orders, users, tickets, logs) ends up with a grid people work through all day. A table that is fine with twenty rows falls apart at five thousand: scrolling stutters, sorting freezes, editing a cell means opening a form. Beamline's [data grid](https://beamline.io/components/data-grid) is built for the five thousand, and the [CRM](https://beamline.io/components/crm) block is a whole pipeline screen on top of it. This guide covers both.

![The Beamline data grid: pinned columns, filters, status and tag cells, footer totals](https://beamline.io/components/og/data-grid.png)

## The grid

```tsx
import { useState } from "react";
import { DataGrid, type DataGridColumn } from "@beamline/data-grid";

type Deal = { id: string; name: string; stage: string; amount: number; probability: number; closeDate: string; tags: string[] };
const columns: DataGridColumn<Deal>[] = [
  { id: "name", header: "Deal", pinned: true, sortable: true, editable: true },
  { id: "stage", header: "Stage", type: "status", filterable: true },
  { id: "amount", header: "Amount", type: "currency", sortable: true, aggregate: "sum" },
  { id: "probability", header: "Probability", type: "percent", aggregate: "avg" },
  { id: "closeDate", header: "Close date", type: "date", sortable: true },
  { id: "tags", header: "Tags", type: "tags" },
];

export function DealsGrid() {
  // Sample data. Edits stay in this component; connect your persistence handler for a product.
  const [deals, setDeals] = useState<Deal[]>([
    { id: "acme", name: "Acme renewal", stage: "Proposal", amount: 24000, probability: 0.7, closeDate: "2026-11-01", tags: ["Renewal"] },
  ]);
  return (
    <DataGrid
      data={deals}
      columns={columns}
      getRowId={(deal) => deal.id}
      onCellEdit={(rowId, columnId, value) => {
        if (columnId === "name" && typeof value === "string") {
          setDeals((rows) => rows.map((row) => row.id === rowId ? { ...row, name: value } : row));
        }
      }}
    />
  );
}
```

Columns have a type (`text`, `number`, `currency`, `percent`, `date`, `status`, `tags`) that sets how a cell is drawn, formatted, sorted and edited. On top of that:

* **Thousands of rows:** the grid is virtualised, so only the rows in view exist in the page.
* **Sort and filter:** multi-column sort, a search over every column, and value filters built from each column's distinct values.
* **Shape the view:** resizable columns, pinned columns on the start edge, row numbers, density.
* **Edit in place:** double-click, Enter, F2 or just typing starts an edit; Enter commits, Escape cancels, Tab commits and moves on. Your `onCellEdit` gets the row, the column and the value.
* **Select and total:** range selection, and footer aggregates per column (count, sum, average, minimum, maximum, unique).

Sort, filters, search and selection each come in both forms (`sort` / `defaultSort` / `onSortChange` and so on), so the grid can own them or your URL can.

## Missing cells say so

A cell with no value is not drawn as 0 or as an empty string. It shows the column's `nullLabel`, and the footer's totals say how many rows they cover, so an average over half the rows does not pass for an average over all of them.

## The keyboard is the fast way

The grid is one tab stop with the whole keyboard model inside: arrows move between cells, Home and End go to the row's edges, Ctrl+Home and Ctrl+End to the grid's corners, Page Up and Page Down by a screen. People who live in a grid stop reaching for the mouse.

## Measured, not assumed

Scrolling 5,000 rows is in Beamline's speed bench. Its first run found the footer totals recomputed over every row on every render, three renders per scroll step; memoising them cut scrolling's main-thread time by 18.7 % with nothing else changing ([the measurements](https://beamline.io/guides/react-performance-lessons)).

## The CRM screen

The [CRM](https://beamline.io/components/crm) block is a pipeline built on the grid: KPIs and the stage mix are derived from the deals, the grid takes inline edits, bulk moves and deletes with undo, and a row opens the deal in a side sheet with a stage stepper, fields and notes, while the list stays in view. Every change goes to your `onDealsChange`:

```tsx
import { Crm, type CrmProps } from "@beamline/crm";

export function Pipeline({ deals, onDealsChange: save, owners, me }: CrmProps) {
  return <Crm deals={deals} onDealsChange={save} owners={owners} me={me} />;
}
```

It fits any record-heavy B2B screen: deals, accounts, tickets, orders. Enter on a row opens it, Escape closes the sheet and returns focus to the row, and a delete asks first, with Escape to back out.

## When to use something else

* A handful of static rows in a card: a [table](https://beamline.io/components/table), not a grid.
* Fewer than about 50 records people mostly read: an [item](https://beamline.io/components/item) list.
* Trading positions with live profit and loss: the [positions table](https://beamline.io/components/positions-table).
* A read-only report: the [analytics dashboard](https://beamline.io/guides/react-analytics-dashboard).

## Ask your agent for it

With Beamline connected ([setup for your agent](https://beamline.io/connect)): "a deals pipeline on our /api/deals with inline editing, stage filters and a detail panel with notes". The agent installs the CRM block (or the grid alone for a simpler list), maps your fields to columns with the right types and wires edits to your API.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://beamline.gitbook.io/beamline-docs/guides/build-a-data-grid-and-crm-screen-in-react.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
