react-data-grid is a data grid with zero dependencies (peer dependency: React 19.2+). It uses CSS Grid for layout and implements row/column virtualization in JS.
src/
index.ts # public API surface; all exports go through here
DataGrid.tsx # main <DataGrid> component (generic: <R, SR, K>)
TreeDataGrid.tsx # wraps DataGrid, adds row grouping (role="treegrid")
types.ts # shared type definitions (e.g., Column, CalculatedColumn, render props, events)
hooks/ # shared custom React hooks
utils/ # pure utilities (e.g., keyboard, DOM, events, colSpan, style)
style/ # build-time CSS via ecij tagged templates; layers.css declares @layer order
cellRenderers/ # default cell renderers (e.g., checkbox, toggleGroup, value)
editors/ # default editors (renderTextEditor)
test/
browser/ # vitest browser-mode tests (Playwright: Chromium, Firefox, WebKit)
node/ # vitest SSR tests (Node.js)
visual/ # vitest visual regression tests (CI-only — never run locally)
website/ # demo site (Vite + TanStack Router)
- Public API — all exports flow through
src/index.ts. KeepREADME.mdin sync with user-facing changes. - Docs — keep
AGENTS.mdin sync with tooling, conventions, or architectural changes. - Default renderers —
DataGridRenderersContextallows overriding default renderers (renderCheckbox,renderSortStatus,renderRow,renderCell,noRowsFallback) without prop-drilling. - TypeScript strict with
exactOptionalPropertyTypes,verbatimModuleSyntax,erasableSyntaxOnly. Distinguish missing properties fromundefinedvalues. Maybe<T>(T | undefined | null) — used for all nullable column/render props. Do not use bareT | undefined.NoInfer<>— wrap callback parameters to prevent reverse type inference into component generics.- Stable object shapes — objects read in hot rendering paths, like calculated columns, must share one hidden class so property reads stay monomorphic: create them with every property in the same order,
undefinedwhen unset, and do not spread user-provided objects into them. - No generators in rendering code — React Compiler can't compile components or hooks that use
yield, and iterating over generator objects is much slower than over arrays. Build arrays with plain loops instead (seesrc/utils/viewportUtils.ts). - CSS layers — all styles live in nested
@layer rdg.<Name>sub-layers (e.g.,rdg.Cell,rdg.Row). When a sub-layer overrides another sub-layer's styles, declare both insrc/style/layers.css, which sets their order; undeclared sub-layers come after the declared ones. Useecijcsstagged templates (build-time extraction, not runtime CSS-in-JS). Co-locate styles in component files;src/style/is for shared styles. - Dual class names — components apply both a semantic class (
rdg-cell) and a generated hash. Preserve both. - Light/dark mode — handled via CSS
light-dark()+color-scheme, not JS. - Accessibility first — ARIA attributes (e.g.,
aria-colindex,aria-rowindex,aria-selected, roles) are required. Tests query by role. - Build —
vp packbundles the library tolib/, then lints the packed package with publint;ecijplugin prefixes classes withrdg-{version}-(dots→dashes) to avoid cross-version conflicts. React Compiler compiles every component and hook, andvp packfails in CI and agent sessions when it can't compile one. - Workflows — pin every action to a full commit SHA with its version in a comment (Dependabot updates both), pass expressions to
runsteps throughenvinstead of inlining${{ }}, grant each job only thepermissionsit uses, and check out withpersist-credentials: false. Jobs that install dependencies only get read permissions: publishing, pushing, and editing PRs happen in separate jobs that receive build output as artifacts and never install the project's dependencies. - Releases — the header of
.github/workflows/publish.ymlexplains how to publish. Its read-onlybuildjob handslib/to thepublishjob, so the project's dependencies never run with theid-token: writepermission that npm's trusted publishing relies on, and a separatereleasejob creates the GitHub release.
- Browser tests use
vite-plus/test/browser+ Playwright.test/setupBrowser.tsconfigurespage.render()viavitest-browser-reactand registers custom locators vialocators.extend()— preferpage.getGrid(),page.getCell({ name }),page.getRow(),page.getHeaderCell(),page.getActiveCell(), etc. over rawpage.getByRole(). - Test helpers in
test/browser/utils.tsx:setup(),getRowWithCell(),getCellsAtRowIndex(),validateCellPosition(),scrollGrid(),testCount(),testRowCount(). test/failOnConsole.tsfails tests on unexpected console warnings/errors.- Firefox runs test files in parallel. Firefox sends the mouse events of all its pages with the same pointer ID, so each test file that drags a resize or drag-fill handle, which captures the pointer, runs in a separate Firefox instance with a browser of its own: add such test files to
firefoxPointerCaptureTestsinvite.config.ts. Use--project "*firefox*"to run all the Firefox instances. - Never run visual regression tests — screenshots are environment-dependent, so visual regression tests must run in CI only.
The project uses Vite+, whose global vp CLI wraps Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Run vp directly, not through npx. Its docs are in node_modules/vite-plus/docs.
vp <command>runs a built-in command, e.g.,vp dev,vp check,vp test,vp pack,vp build.vp run <task>runs a task fromvite.config.ts, e.g.,vp run typecheck,vp run eslint,vp run ci.- Run
vp installafter pulling changes.
Run vp run ci before submitting changes. It runs the same checks as CI, and skips any whose inputs haven't changed since they last passed.
While iterating, run individual checks instead. The static checks take seconds, so run them before tests:
vp run typechecktype checks withtsc --build;vp checkdoes not type check.vp check --fixformats and lints, fixing what it can. Without--fix, a formatting error stops it before linting.vp run eslintlints Markdown files.vp test <files>runs specific test files, e.g.,vp test test/browser/sorting.test.tsx. Add--project "browser (chromium)"to test in a single browser, or--project nodeto run the SSR test. The full suite takes about a minute.