
# Build a data grid and CRM screen in React

By Danylo Pravda, 2026-10-08

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 data grid, on demo data")

## The grid

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

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" },
];

<DataGrid
  data={deals}
  columns={columns}
  getRowId={(d) => d.id}
  onCellEdit={(rowId, columnId, value) => api.patch(`/deals/${rowId}`, { [columnId]: value })}
/>
```

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 } from "@beamline/crm";

<Crm deals={deals} onDealsChange={(next) => save(next)} 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.
