npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reopt-ai/opt-datagrid

v1.5.0

Published

Keyboard-first editable React data grid focused on spreadsheet-like UX and performance.

Downloads

285

Readme

@reopt-ai/opt-datagrid

Keyboard-first editable React data grid package focused on spreadsheet-like UX and practical performance.

Goals

  • Fast editing loop for keyboard users
  • Independent implementation with minimal dependencies
  • Large dataset friendliness via row windowing

Agent skill setup (recommended)

From the consumer project root, install the dedicated skill:

npx skills add reopt-ai/reopt-skills/opt-datagrid-install

Then ask your agent: Use the opt-datagrid-install skill to set up @reopt-ai/opt-datagrid in this project and verify the result. The skill distinguishes a new install from an upgrade, supports migrations from other grid packages, idempotently updates the reopt marker block in AGENTS.md (or CLAUDE.md), reads this installed package's dist/docs/, and runs the appropriate checks. The source of truth is reopt-ai/reopt-skills.

Use the command below as the manual fallback.

Install

bun add @reopt-ai/opt-datagrid

@reopt-ai/opt-datagrid is published publicly on npmjs.org, so consumers do not need a custom registry or authentication configuration.

Requires React 19 and a modern browser. No CSS import is needed. Full guides and interactive examples are available in the opt-datagrid documentation.

Usage

"use client";

import * as React from "react";
import { DataGrid, type DataGridColumn } from "@reopt-ai/opt-datagrid";

interface UserRow {
  id: number;
  name: string;
  role: string;
}

const columns: DataGridColumn<UserRow>[] = [
  {
    id: "name",
    title: "Name",
    width: 220,
    editable: true,
    getValue: (row) => row.name,
    setValue: (row, nextValue) => ({ ...row, name: nextValue }),
  },
  {
    id: "role",
    title: "Role",
    width: 180,
    editable: true,
    getValue: (row) => row.role,
    setValue: (row, nextValue) => ({ ...row, role: nextValue }),
  },
];

export function Example() {
  const [rows, setRows] = React.useState<UserRow[]>([
    { id: 1, name: "Mina", role: "Owner" },
    { id: 2, name: "Joon", role: "Editor" },
  ]);

  return (
    <DataGrid
      rows={rows}
      columns={columns}
      getRowId={(row) => String(row.id)}
      onRowsChange={(nextRows) => setRows(Array.from(nextRows))}
      height={420}
    />
  );
}

Typed editors

DataGridColumn is value-aware. Text columns still work unchanged, but columns can now declare typed editors directly:

import type { DataGridAnyColumn } from "@reopt-ai/opt-datagrid";

interface MemberRow {
  id: number;
  status: "pending" | "active";
  enabled: boolean;
  quota: number | null;
  dueDate: string;
}

const columns: DataGridAnyColumn<MemberRow>[] = [
  {
    id: "status",
    title: "Status",
    editable: true,
    editor: {
      kind: "select",
      options: [
        { value: "pending", label: "Pending" },
        { value: "active", label: "Active" },
      ],
    },
    getValue: (row) => row.status,
    setValue: (row, nextValue) => ({ ...row, status: nextValue }),
  },
  {
    id: "enabled",
    title: "Enabled",
    editable: true,
    editor: { kind: "checkbox" },
    getValue: (row) => row.enabled,
    setValue: (row, nextValue) => ({ ...row, enabled: nextValue }),
    formatValue: (value) => (value ? "Enabled" : "Disabled"),
  },
  {
    id: "quota",
    title: "Quota",
    editable: true,
    editor: { kind: "number", min: 0, step: 1 },
    getValue: (row) => row.quota,
    setValue: (row, nextValue) => ({ ...row, quota: nextValue }),
    formatValue: (value) => (value == null ? "" : String(value)),
  },
  {
    id: "dueDate",
    title: "Due date",
    editable: true,
    editor: { kind: "date" },
    getValue: (row) => row.dueDate,
    setValue: (row, nextValue) => ({ ...row, dueDate: nextValue }),
  },
];

Built-in editor kinds:

  • "text" and "textarea"
  • "number"
  • "select"
  • "checkbox"
  • "date"
  • "async-combobox"
  • "custom"

Useful value hooks:

  • formatValue for cell display text
  • serializeValue for copy / export / search-friendly string output
  • deserializeValue for paste / edit parsing into typed values
  • parseInput for custom draft parsing
  • validateValue for typed validation before commit

Custom editors are attached at the column level via component:

import { type DataGridEditorContext } from "@reopt-ai/opt-datagrid";

function StatusEditor(
  context: DataGridEditorContext<MemberRow, MemberRow["status"]>,
) {
  const { setDraftValue, commit, cancel } = context;

  return (
    <div style={{ display: "grid", gap: 8 }}>
      <button
        type="button"
        onClick={() => {
          setDraftValue("pending");
          commit();
        }}
      >
        Pending
      </button>
      <button
        type="button"
        onClick={() => {
          setDraftValue("active");
          commit();
        }}
      >
        Active
      </button>
      <button type="button" onClick={cancel}>
        Cancel
      </button>
    </div>
  );
}

const columns: DataGridColumn<MemberRow, MemberRow["status"]>[] = [
  {
    id: "status",
    title: "Status",
    editable: true,
    editor: {
      kind: "custom",
      component: StatusEditor,
    },
    getValue: (row) => row.status,
    setValue: (row, nextValue) => ({ ...row, status: nextValue }),
  },
];

component is the custom editor API for reusable editors.

Theme tokens (opt-ui auto integration)

DataGrid reads opt-ui CSS variables when available and falls back to built-in defaults when they are not present.

Common tokens used by default styles:

  • --opt-surface, --opt-surface-raised
  • --opt-text, --opt-text-secondary, --opt-text-tertiary
  • --opt-border, --opt-border-subtle, --opt-border-hover
  • --opt-accent, --opt-accent-active, --opt-accent-subtle, --opt-accent-fg
  • --opt-ring
  • --opt-info-subtle, --opt-warning, --opt-warning-subtle
  • --opt-shadow-sm, --opt-shadow-md, --opt-shadow-lg

Keyboard

  • Arrow keys: move active cell
  • Ctrl/Cmd + Arrow: jump to row/column boundary
  • Shift + Arrow: extend range selection
  • Enter / F2: start editing
  • Edit mode commit:
    • Enter down, Shift+Enter up
    • Tab right, Shift+Tab left
    • Ctrl/Cmd+Enter commit without moving
  • Tab / Shift+Tab: horizontal move (wrap behavior controlled by keyboardFlow.wrapMode)
  • Home / End: row boundary
  • Ctrl/Cmd + Home / End: grid boundary
  • Ctrl/Cmd + A: select all
  • Ctrl/Cmd + C: copy selected cells
  • Ctrl/Cmd + X: cut selected cells
  • Ctrl/Cmd + V: paste clipboard payload
  • Ctrl/Cmd + F: open in-grid search
  • Ctrl/Cmd + Z / Y: undo / redo edits

Keyboard flow options

DataGrid supports directional and continuous edit flow:

  • keyboardFlow.continuousEdit (default: true)
  • keyboardFlow.defaultDirection ("right" or "down", default: "right")
  • keyboardFlow.wrapMode ("row" or "none", default: "row")
  • keyboardFlow.skipReadonly (default: true)

Use onFinishedEditing to observe accepted commit + movement direction.

Advanced hooks

  • Edit pipeline:
    • onCellCommit, onCellsEdited, onCopy, onCut, onPaste, onDelete
    • onPaste returns "apply" or "block"
    • onCopy/onCut return { action: "default" | "block" | "override" }
  • Validation pipeline: validateCell, coercePasteValue
  • Typed editor hooks:
    • editor, formatValue, serializeValue, deserializeValue
    • parseInput, validateValue
    • dependsOnColumnIds, areValuesEqual
  • Selection control: initialActiveCell, gridSelection, onGridSelectionChange
  • Row/Column selection: rowMarkers, rowSelect, columnSelect
    • row marker clicks preserve row selection while moving the active cell within that row

AI stream invalidations

Remote grids can share the same AI SDK UI message stream as chat or agent workflows. Use writeDataGridInvalidation on the server and parseDataGridInvalidationPart on the client to keep streamed invalidations typed and dedupe-friendly:

import {
  parseDataGridInvalidationPart,
  writeDataGridInvalidation,
} from "@reopt-ai/opt-datagrid/ai-stream";

writeDataGridInvalidation(
  writer,
  {
    rows: [{ rowId: "row-1", row: { id: "row-1", status: "approved" } }],
    totalRowCount: 2500,
    snapshotVersion: "snap-42",
  },
  { id: "grid-invalidation-42", transient: true },
);

const invalidation = parseDataGridInvalidationPart<Row>(part);
  • Column control: onColumnsChange, onColumnResize, onColumnMoved
  • Header actions: onHeaderClicked, onHeaderMenuClick, onHeaderMenuAction
    • header title buttons support pointer and keyboard activation
  • Viewport events: onVisibleRegionChanged + rowBufferPx
  • Clipboard controls:
    • copyHeaders (include header row in copy output)
    • maxPasteCells, onPasteOverflow
    • getCellsForSelection (sync/async custom copy source)
  • Performance tuning:
    • searchDebounceMs (default 80)
    • selectionCacheStrategy ("set" or "none", default "set")
    • scrollUpdateMode ("raf" or "sync", default "raf")
    • getRowId to stabilize row identity across reorder, insert, and sort
    • valueCache to reuse getValue results across interaction-only rerenders
    • valueCacheStrategy ("row-ref" or "row-id") to control cache identity
    • rendererRefreshMode to control renderer-host reuse
    • rowBufferPx to control pixel-based row buffering
    • maxRenderedRows to clamp pathological render windows
  • Search control: showSearch, searchValue, searchResults
  • Imperative ref API:
    • focus, scrollTo, getBounds
    • appendRow, appendColumn, getMouseArgsForPosition
    • copySelection, cutSelection, pasteText, pasteFromClipboard
    • deleteSelection, fillSelectionRight, fillSelectionDown

Value cache and renderer reuse

When valueCache is enabled, DataGrid caches cell values by row identity and column id so active-cell movement, selection, and other interaction-only updates do not keep re-running getValue.

By default the cache uses row references. If your grid frequently reorders, sorts, or inserts rows, provide getRowId so the cache and rendered row keys stay warm across index changes:

<DataGrid
  rows={rows}
  columns={columns}
  valueCache
  getRowId={(row) => String(row.id)}
  valueCacheStrategy="row-id"
/>

For derived columns, declare dependencies with dependsOnColumnIds so cache invalidation can stay narrow:

const columns: DataGridColumn<UserRow>[] = [
  {
    id: "name",
    title: "Name",
    editable: true,
    getValue: (row) => row.name,
    setValue: (row, nextValue) => ({ ...row, name: nextValue }),
  },
  {
    id: "summary",
    title: "Summary",
    dependsOnColumnIds: ["name", "role"],
    getValue: (row) => `${row.name} · ${row.role}`,
  },
];

If you use renderCell or customRenderers, you can also provide a refresh predicate to keep unchanged renderers from rerunning when sibling cells update:

{
  id: "status",
  title: "Status",
  getValue: (row) => row.status,
  renderCell: ({ value }) => <StatusBadge value={value} />,
  refreshCellRenderer: (prev, next) => prev.value === next.value,
}

Remote data source

Use useDataGridRemoteDataSource when the backend is designed around viewport windows, view sessions, and batched edits:

const remote = useDataGridRemoteDataSource<Row>({
  rowCount: 120000,
  pageSize: 200,
  preloadPages: 1,
  getVisibleColumnIds: (region) =>
    columns
      .slice(region.startCol, region.endCol + 1)
      .map((column) => column.id),
  openView: async ({ signal }) => {
    const response = await fetch("/api/grid/views/open", {
      method: "POST",
      signal,
    });
    const payload = await response.json();
    return {
      viewId: payload.viewId,
      rowCount: payload.totalRowCount,
      snapshotVersion: payload.snapshotVersion,
    };
  },
  subscribeToInvalidations: ({ viewId, onInvalidate }) => {
    const events = new EventSource(`/api/grid/views/${viewId}/events`);
    events.addEventListener("invalidate", (event) => {
      const payload = JSON.parse((event as MessageEvent<string>).data);
      onInvalidate({
        rows: payload.rows,
        rowCount: payload.totalRowCount,
        snapshotVersion: payload.snapshotVersion,
        movedRowIds: payload.movedRowIds,
        invalidateRanges: payload.invalidateRanges,
      });
    });
    return () => events.close();
  },
  makePlaceholderRow: (rowIndex) => ({
    id: `loading-${rowIndex}`,
    name: "Loading...",
  }),
  loadRows: async ({ start, end, viewId, visibleColumnIds, signal }) => {
    const params = new URLSearchParams({
      start: String(start),
      end: String(end),
    });
    if (visibleColumnIds?.length) {
      params.set("columns", visibleColumnIds.join(","));
    }
    const response = await fetch(
      `/api/grid/views/${viewId}/window?${params.toString()}`,
      {
        signal,
      },
    );
    const payload = await response.json();
    return {
      rows: payload.rows,
      rowCount: payload.totalRowCount,
      snapshotVersion: payload.snapshotVersion,
    };
  },
  saveEdits: async ({ edits, viewId, snapshotVersion, signal }) => {
    const response = await fetch(`/api/grid/views/${viewId}/edits`, {
      method: "POST",
      signal,
      body: JSON.stringify({ edits, snapshotVersion }),
    });
    const payload = await response.json();
    return {
      rows: payload.rows,
      snapshotVersion: payload.snapshotVersion,
      movedRowIds: payload.movedRowIds,
      invalidateRanges: payload.invalidateRanges,
      rejectedEdits: payload.rejectedEdits,
    };
  },
  revalidateAfterSave: "affected-pages",
});

<DataGrid
  rows={remote.rows}
  columns={columns}
  onVisibleRegionChanged={remote.onVisibleRegionChanged}
  onCellsEdited={remote.onCellsEdited}
/>;

if (remote.rejectedEdits.length > 0) {
  console.warn(remote.rejectedEdits);
}

console.log(remote.telemetry);

Backend contract checklist:

  • openView should materialize or cache a stable sorted/filtered view and return viewId, rowCount, and snapshotVersion.
  • loadRows should accept start, end, and visibleColumnIds so the server can do range fetch plus column projection.
  • saveEdits should handle batched cell patches and return canonical rows, rejectedEdits, movedRowIds, and invalidateRanges when needed.
  • subscribeToInvalidations should push the same invalidation shape for multi-user changes on the same viewId.

Error handling (remote data source)

useDataGridRemoteDataSource exposes granular error state for building resilient UIs. Use these fields to show inline error indicators, retry controls, and fallback states.

Error state fields

| Field | Type | Description | | --------------- | ------------------------------ | ------------------------------------------- | | lastViewError | Error \| null | openView failure — grid cannot initialize | | lastLoadError | Error \| null | Most recent loadRows failure | | lastSaveError | Error \| null | Most recent saveEdits failure | | failedPages | number[] | Page indices that failed to load | | failedEdits | DataGridCellEdit[] | Edits that could not be saved | | rejectedEdits | DataGridRemoteRejectedEdit[] | Server-rejected edits (e.g. validation) | | isViewReady | boolean | false until openView succeeds | | isViewOpening | boolean | true while openView is in flight |

Recovery methods

| Method | Description | | ----------------------- | ------------------------------------ | | retryPage(page) | Re-fetch a single failed page | | retryFailedEdits() | Re-submit all failed edits | | refreshVisiblePages() | Force-reload currently visible pages |

Example: error boundary pattern

function RemoteGrid({ columns }: { columns: DataGridAnyColumn<Row>[] }) {
  const remote = useDataGridRemoteDataSource<Row>({/* ... */});

  // 1. View initialization error — show full-page fallback
  if (remote.lastViewError) {
    return (
      <div role="alert">
        <p>그리드를 초기화할 수 없습니다: {remote.lastViewError.message}</p>
        <button onClick={() => window.location.reload()}>새로고침</button>
      </div>
    );
  }

  // 2. View still opening — show skeleton
  if (!remote.isViewReady) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {/* 3. Page-level load errors — inline banner */}
      {remote.failedPages.length > 0 && (
        <div role="alert">
          {remote.failedPages.length}개 페이지 로드 실패
          <button onClick={() => remote.failedPages.forEach(remote.retryPage)}>
            재시도
          </button>
        </div>
      )}

      {/* 4. Save errors — toast or inline */}
      {remote.lastSaveError && (
        <div role="alert">
          저장 실패: {remote.lastSaveError.message}
          <button onClick={remote.retryFailedEdits}>재시도</button>
        </div>
      )}

      {/* 5. Server-rejected edits — field-level feedback */}
      {remote.rejectedEdits.length > 0 && (
        <div role="alert">
          {remote.rejectedEdits.map((r, i) => (
            <p key={i}>
              Column "{r.columnId}": {r.message ?? "서버에서 거부됨"}
            </p>
          ))}
        </div>
      )}

      <DataGrid
        rows={remote.rows}
        columns={columns}
        height={600}
        onVisibleRegionChanged={remote.onVisibleRegionChanged}
        onCellsEdited={remote.onCellsEdited}
      />
    </div>
  );
}

Error hierarchy

  1. View error (lastViewError): fatal — grid cannot render. Show full fallback.
  2. Page errors (failedPages): partial — some rows show placeholders. Offer per-page retry.
  3. Save errors (lastSaveError, failedEdits): optimistic edits rolled back. Offer batch retry.
  4. Rejected edits (rejectedEdits): server validation failures. Show field-level messages.

Utilities

  • useDataGridUndoRedo: standalone undo/redo history stack utility
  • useDataGridRemoteDataSource: viewport-driven remote read/write source with optimistic edit batching
  • useAsyncDataSource: read-only convenience wrapper over useDataGridRemoteDataSource

Development

bun run --filter @reopt-ai/opt-datagrid test
bun run --filter @reopt-ai/opt-datagrid typecheck
bun run --filter @reopt-ai/opt-datagrid build

Maintainer release

유지보수자 배포는 직접 npm publish 또는 bun run release*를 사용하지 않고 design-publish 워크플로우만 사용합니다.

Benchmarking

Run the local benchmark harness to compare practical grid scenarios without adding cost to the regular test suite:

bun run --filter @reopt-ai/opt-datagrid benchmark

Useful options:

  • --rows <n> and --cols <n> to scale the dataset
  • --iterations <n> to control measured repetitions per scenario
  • --scenario <list> to run a subset such as active-move,search-refine
  • --baseline [path] to compare the current run against a saved JSON baseline
  • --save-baseline [path] to write the current run as a reusable baseline
  • --history [path] to append the current run to a history JSON and print a recent trend table
  • --history-limit <n> to control how many recent reports the trend summary considers
  • --fail-on-regression to exit non-zero when compared metrics regress beyond the threshold
  • --regression-threshold <n> to control regression sensitivity (default 5)
  • --json to emit machine-readable output

Example:

bun run --filter @reopt-ai/opt-datagrid benchmark -- --rows 10000 --cols 100 --iterations 5

Default baseline helpers:

bun run --filter @reopt-ai/opt-datagrid benchmark:save-baseline
bun run --filter @reopt-ai/opt-datagrid benchmark:compare
bun run --filter @reopt-ai/opt-datagrid benchmark:history