RevoGrid in React: Installation, Virtualization, and Custom Editors — A Complete Guide
Every few years the JavaScript ecosystem rediscovers that rendering 100,000 rows of tabular data is,
in fact, hard. Libraries come and go, usually carrying one of two sins: they’re either too simple to
matter in production, or they’re so over-engineered that configuring a column header requires a
philosophy degree. RevoGrid
sits in the uncomfortable middle — in the best possible way. Built on
Stencil.js as a
framework-agnostic Web Component, it ships native React wrappers and delivers
virtual scrolling, custom cell editors, and Excel-like interaction patterns without
asking you to abandon your existing React architecture.
This guide is a hands-on walkthrough: from bare-bones
revo-grid installation through
virtualization internals to
custom editors that behave exactly like those in enterprise
spreadsheet tools. By the end, you’ll have a production-ready
React spreadsheet component capable of handling enterprise-scale datasets —
no magic, no hand-waving.
Why RevoGrid When AG Grid and Handsontable Exist?
Fair question. AG Grid is the incumbent — battle-tested, extensively documented,
and capable of almost anything. The catch is the Community/Enterprise paywall: the moment you need
row grouping, pivoting, or server-side sorting with full UX parity, you’re on the Enterprise plan.
Handsontable changed its license in 2019 and commercial projects now require a paid
seat. RevoGrid is MIT-licensed, and that alone reframes the conversation for a large number of teams.
The second reason is the Web Components architecture. Because
revo-grid is
compiled to a standard custom element, it works identically in React, Vue, Angular, Svelte, or
plain HTML. Your spreadsheet logic isn’t coupled to React’s render cycle — which means fewer
surprises when you upgrade React versions, and a realistic path to micro-frontend architectures
where different teams use different frameworks.
Third: raw performance. RevoGrid virtualizes both rows and columns simultaneously,
uses a minimal DOM footprint, and batches DOM mutations aggressively. In informal benchmarks
involving 200k rows and 50 columns, scroll performance stays above 60 fps on mid-range hardware.
That’s not marketing copy — that’s what virtual rendering buys you when the implementation is
competent. And for React data grid performance at that scale, the alternatives
are either expensive or impractical.
Installation and Project Setup
The revo-grid setup process is deliberately minimal. The library ships both
the raw Web Component loader and a React-specific wrapper package. For React projects you’ll
want the wrapper — it handles ref forwarding and event bridging so you don’t have to manually
wire DOM events into React state.
# Install the core library and React wrapper
npm install @revolist/revogrid @revolist/react-datagrid
# If you're on Yarn
yarn add @revolist/revogrid @revolist/react-datagrid
With the packages installed, you need to register the Web Component before the first render.
The canonical place is your application’s entry point — typically main.tsx or
index.tsx. Skipping this step is the single most common reason people open
GitHub issues saying “the grid renders nothing.”
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { defineCustomElements } from '@revolist/revogrid/loader';
import App from './App';
// Register the revo-grid custom element globally
defineCustomElements();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Now pull the React wrapper into your component. The RevoGrid export from
@revolist/react-datagrid is a thin React component that maps props to the
underlying custom element’s attributes and properties, and maps custom events to standard
React-style callback props. You define columns as an array of
ColumnRegular objects and pass rows as plain JavaScript objects — no class
instances, no observable decorators, no ceremony.
// SpreadsheetDemo.tsx
import React, { useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import type { ColumnRegular, DataType } from '@revolist/revogrid';
const columns: ColumnRegular[] = [
{ prop: 'id', name: 'ID', size: 60, readonly: true },
{ prop: 'product', name: 'Product', size: 200 },
{ prop: 'category', name: 'Category', size: 150 },
{ prop: 'price', name: 'Price (USD)', size: 120 },
{ prop: 'stock', name: 'In Stock', size: 100 },
];
const generateRows = (count: number): DataType[] =>
Array.from({ length: count }, (_, i) => ({
id: i + 1,
product: `Product ${i + 1}`,
category: i % 3 === 0 ? 'Electronics' : i % 3 === 1 ? 'Apparel' : 'Food',
price: +(Math.random() * 500 + 10).toFixed(2),
stock: Math.floor(Math.random() * 1000),
}));
export const SpreadsheetDemo: React.FC = () => {
const [rows] = useState<DataType[]>(() => generateRows(50_000));
return (
<div style={{ height: '600px', width: '100%' }}>
<RevoGrid
columns={columns}
source={rows}
theme="material"
resize
filter
exporting
/>
</div>
);
};
<RevoGrid> in a container with anexplicit pixel or viewport height. The component sizes itself to fill its parent — if the
parent has
height: auto, you’ll get a zero-height grid and a puzzled expression.
How RevoGrid Virtualization Actually Works
React virtualized spreadsheet libraries typically implement one of two
strategies: windowing (render only visible rows) or recycling (reuse DOM nodes as the user
scrolls). RevoGrid does both. It maintains a “virtual viewport” — a calculated window of
rows and columns based on the scroll position, container dimensions, and row/column sizes.
Only cells within that window exist in the DOM at any given moment. Everything else is a
mathematical concept.
Column virtualization is where revo-grid virtualization pulls ahead of many
competitors. Most “virtualized” grids only virtualize rows, leaving all column DOM nodes
alive simultaneously. With 200 columns, that’s a substantial DOM penalty even if only 10
columns are visible. RevoGrid virtualizes the column axis independently, so a 200-column
dataset with 10 visible columns costs roughly the same as a 10-column dataset. This matters
enormously in financial dashboards and analytics UIs where wide tables are the norm.
You can observe the virtual rendering behavior directly by opening DevTools while scrolling.
The number of <td>-equivalent elements in the DOM stays constant
regardless of dataset size — it’s proportional to visible cells only. For
React high-performance grid implementations, this is the architectural
decision that separates tools that work in demos from tools that survive production load.
RevoGrid also exposes rowSize and colSize props to set uniform
row and column heights when you know them upfront, which allows the engine to skip
expensive layout measurements during scroll.
Column Configuration: Beyond the Basics
A ColumnRegular object in RevoGrid is deceptively flexible. The obvious
properties — prop, name, size — are just the entry
point. The interesting work happens through cellTemplate,
columnTemplate, editor, filter, and
columnType. These let you transform a basic data grid into something that
genuinely resembles an Excel-like React component with application-specific
logic baked into individual columns.
Cell templates are particularly powerful. Rather than configuring a cell renderer class
(AG Grid’s pattern), RevoGrid accepts a function that receives a cell context object and
returns a virtual DOM node via the h hyperscript function. This keeps rendering
logic co-located with your column definitions and avoids the class-based ceremony that
makes AG Grid renderers feel like writing jQuery plugins in 2024.
import { h } from '@revolist/revogrid';
import type { ColumnRegular, VNode } from '@revolist/revogrid';
const priceColumn: ColumnRegular = {
prop: 'price',
name: 'Price',
size: 130,
// Custom cell renderer — color-code price bands
cellTemplate: (createElement, props): VNode => {
const value = props.model[props.prop] as number;
const color = value > 300 ? '#dc2626' : value > 100 ? '#d97706' : '#16a34a';
return createElement(
'span',
{ style: { color, fontWeight: '600' } },
`$${value.toFixed(2)}`
);
},
// Column-level filter
filter: 'number',
};
// Frozen (pinned) columns — same API, one extra flag
const idColumn: ColumnRegular = {
prop: 'id',
name: 'ID',
size: 60,
pin: 'colPinStart', // freeze left
readonly: true,
};
Column pinning (pin: 'colPinStart' or 'colPinEnd') is one of
those features that sounds trivial until you try to implement it yourself. RevoGrid handles
the sticky positioning, z-index layering, and scroll synchronization automatically, which
is a non-trivial amount of CSS and scroll event math. For enterprise spreadsheet use cases —
think financial modeling tables or ERP-style data entry screens — pinned columns are
effectively non-negotiable, and having them work out of the box saves days.
Custom Cell Editors: Full Control Over Data Entry
The built-in editors in RevoGrid cover text, number, and select inputs — sufficient for
prototypes, insufficient for production. Revo-grid custom editors fill
the gap. The API is a plain TypeScript class (or object) implementing four lifecycle methods:
element (the DOM node), editCell() (called when editing begins),
getValue() (called when the grid commits the edit), and
disconnectedCallback() (cleanup). No framework magic, no abstractions —
just a thin contract between the grid and your input logic.
// editors/DatePickerEditor.ts
import type { EditorBase } from '@revolist/revogrid';
export class DatePickerEditor implements EditorBase {
element!: HTMLInputElement;
editCell() {
this.element = document.createElement('input');
this.element.type = 'date';
this.element.style.cssText =
'width:100%;height:100%;border:none;padding:0 8px;font-size:inherit;';
// Focus on next tick so the cell is fully mounted
requestAnimationFrame(() => this.element?.focus());
}
getValue() {
return this.element?.value ?? '';
}
disconnectedCallback() {
this.element?.remove();
}
}
Register the editor and wire it to a column by name. The
editors prop on <RevoGrid> accepts a plain object mapping
string keys to editor constructors or factory functions. Column definitions reference the
same key in their editor field. This indirection means you can swap editor
implementations at runtime — useful for feature flags or user-configurable input modes.
// SpreadsheetWithEditors.tsx
import React, { useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import type { ColumnRegular, DataType, Editors } from '@revolist/revogrid';
import { DatePickerEditor } from './editors/DatePickerEditor';
// Editor registry — keys are referenced from column definitions
const editors: Editors = {
'date-picker': DatePickerEditor,
};
const columns: ColumnRegular[] = [
{ prop: 'id', name: 'ID', size: 60, readonly: true },
{ prop: 'description', name: 'Description', size: 250 },
{
prop: 'dueDate',
name: 'Due Date',
size: 160,
editor: 'date-picker', // ← references registry key
},
{ prop: 'status', name: 'Status', size: 120 },
];
const initialRows: DataType[] = [
{ id: 1, description: 'Migrate auth service', dueDate: '2025-08-01', status: 'In Progress' },
{ id: 2, description: 'Update API documentation', dueDate: '2025-07-15', status: 'Pending' },
{ id: 3, description: 'Performance audit', dueDate: '2025-09-01', status: 'Pending' },
];
export const TaskGrid: React.FC = () => {
const [rows, setRows] = useState<DataType[]>(initialRows);
return (
<div style={{ height: '400px' }}>
<RevoGrid
columns={columns}
source={rows}
editors={editors}
theme="material"
onAfterEdit={(e) => {
// e.detail carries { prop, val, rowIndex, model }
const { rowIndex, prop, val } = e.detail;
setRows(prev =>
prev.map((row, i) =>
i === rowIndex ? { ...row, [prop]: val } : row
)
);
}}
/>
</div>
);
};
The onAfterEdit event is where React state reconciliation happens. Notice that
we’re not mutating the original row — we produce a new array with the modified row, keeping
the update immutable and React-friendly. For large datasets, consider using a Map
keyed by row ID instead of array index for O(1) updates rather than O(n) linear scans.
This is a common optimization in React enterprise spreadsheet implementations
where edit frequency is high and dataset size is measured in tens of thousands of rows.
Advanced Patterns: Sorting, Filtering, and Export
Revo-grid advanced functionality — sorting, column filtering, and data
export — is activated largely through props rather than imperative API calls. This is a
deliberate design choice that keeps the component declarative and aligns with React’s
mental model. Enable filter, resize, and exporting
props on the root element and the corresponding UI affordances appear automatically: filter
icons in column headers, resize handles on column edges, and an export function accessible
via the grid’s ref.
// AdvancedGrid.tsx
import React, { useRef } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import type { RevoGridCustomEvent } from '@revolist/revogrid';
export const AdvancedGrid: React.FC<{ rows: any[]; columns: any[] }> = ({
rows,
columns,
}) => {
const gridRef = useRef<HTMLRevoGridElement | null>(null);
const handleExportCSV = async () => {
if (!gridRef.current) return;
// Export to CSV — triggers browser download
await gridRef.current.getPlugins().then(plugins => {
const exportPlugin = plugins.find(p => (p as any).exportFile);
(exportPlugin as any)?.exportFile({ filename: 'data-export', fileType: 'csv' });
});
};
const handleSortChange = (e: RevoGridCustomEvent<any>) => {
console.log('Sort changed:', e.detail);
// Optionally drive server-side sort here
};
return (
<>
<button onClick={handleExportCSV} style={{ marginBottom: '0.75rem' }}>
Export CSV
</button>
<div style={{ height: '600px' }}>
<RevoGrid
ref={gridRef}
columns={columns}
source={rows}
theme="material"
filter // enable column filters
resize // enable column resizing
exporting // enable CSV / XLSX export plugin
canMoveColumns // enable column reordering
onSortingChanged={handleSortChange}
/>
</div>
</>
);
};
Server-side operations deserve a separate mention. For truly large datasets — millions of
rows sourced from a backend — you don’t pass all data upfront. Instead, you listen to
onSortingChanged and onFilterChanged events, fire your API
requests, and update the source prop with the returned page of results.
Combine this with RevoGrid’s virtual scrolling and you get a spreadsheet UX that feels
local even when data lives entirely on the server. This is the pattern underpinning most
serious React Excel component implementations in SaaS products.
Theming is handled through CSS custom properties, which means you can match your design
system without forking the library or wrestling with styled-components injection. The
built-in themes ("material", "compact", "darkMaterial")
serve as sensible baselines. Override specific tokens like
--revo-grid-header-bg or --revo-grid-border-color at the
component level using a CSS class on the wrapper element. For teams building white-label
or heavily branded products, this matters more than most grid libraries acknowledge.
Performance Checklist for Production Deployments
Getting RevoGrid running is easy. Getting it running fast at scale requires a handful of
deliberate choices. The following patterns are distilled from real production implementations
of React data grid performance optimization — not hypothetical advice.
-
Memoize column definitions. Define the
columnsarray outside
the component or wrap it inuseMemo. If the columns reference changes on every
render, the grid re-initializes its internal column model unnecessarily. -
Stabilize row data references. Use
useStateinitializers or
useMemofor thesourceprop. Passing a new array literal on
every render forces the grid to diff the entire dataset. -
Set
rowSizewhen possible. A fixed row height lets the
virtual engine calculate scroll positions mathematically rather than measuring DOM nodes.
The performance difference on datasets over 50k rows is measurable. -
Use
readonlyon non-editable columns. Read-only cells skip
the editor registration and event delegation overhead — small savings per cell, meaningful
savings across 100k cells. -
Batch state updates. If you’re applying multiple edits programmatically
(e.g., paste operations), batch them into a singlesetRowscall rather than
triggering one state update per cell.
Beyond RevoGrid-specific tuning, the usual React performance hygiene applies: avoid
anonymous functions in JSX props, prefer stable keys, and profile with the React DevTools
Profiler before optimizing anything you haven’t measured. Premature optimization in a
data grid context is how engineers spend two days chasing a 3ms improvement while the
actual bottleneck is a synchronous API call on mount.
Memory management is the other dimension. Virtual rendering handles DOM memory, but
application state is your responsibility. For datasets that update in real time (WebSocket
feeds, live dashboards), avoid accumulating all historical rows in React state. Maintain
a sliding window — say, the most recent 10,000 rows — and expose historical data through
a separate view or pagination. RevoGrid’s source prop can be updated incrementally;
the grid handles reconciliation efficiently as long as you’re not replacing the entire
array on every tick.
RevoGrid as a Web Component: What It Means for Your Stack
The revo-grid Web Components foundation is a genuine architectural
advantage, not a marketing bullet point. Because the core is a standard custom element,
it doesn’t carry React as a dependency. This has two practical consequences: the bundle
size is smaller than a pure-React grid of equivalent capability, and the component works
in non-React contexts without modification. If you’re building a micro-frontend
architecture where a Vue team and a React team need to share UI primitives, RevoGrid
is one of the few data grid options that genuinely supports that scenario.
The React wrapper (@revolist/react-datagrid) adds event bridging and ref
forwarding so you interact with the grid using React idioms rather than imperative DOM
calls. Under the hood, custom events emitted by the Web Component are mapped to
onEventName props, and the grid’s imperative API (scroll to row, get current
filter state, trigger export) is accessible via a standard React ref. The wrapper is thin
by design — it doesn’t reimplement the grid in React, it adapts the existing Web Component
to React’s expectations.
One edge case worth knowing: React’s synthetic event system doesn’t capture custom DOM
events by default. The wrapper handles this, but if you’re using the raw
<revo-grid> custom element directly in a React app (instead of the
wrapper), you’ll need to attach listeners via a ref using
element.addEventListener('afteredit', handler). This is a standard Web
Components integration pattern, not a RevoGrid quirk — but it trips up developers who
expect React’s JSX event props to work on arbitrary custom elements.
Frequently Asked Questions
How do I install and set up RevoGrid in a React project?
Install the packages via npm: npm install @revolist/revogrid @revolist/react-datagrid.
Then call defineCustomElements() from @revolist/revogrid/loader
in your entry file (main.tsx) before the React app mounts. Finally, import
RevoGrid from @revolist/react-datagrid and place it inside a
container element with an explicit height. The grid sizes itself to fill its parent, so
height: 0 on the wrapper means an invisible grid.
Can RevoGrid handle large datasets without performance issues?
Yes — it’s built for exactly this scenario. RevoGrid virtualizes both rows and columns
simultaneously, keeping the live DOM proportional to visible cells rather than total
data size. With the correct setup (fixed rowSize, memoized columns,
stable source references), it handles 100k+ rows at smooth 60fps scroll performance.
For datasets beyond a few hundred thousand rows, pair virtual rendering with
server-side pagination triggered by the grid’s sort/filter events.
How do I create a custom cell editor in RevoGrid?
Implement a class with four methods: editCell() (create and mount your
input element), getValue() (return the current editor value),
disconnectedCallback() (cleanup), and an element property
pointing to the mounted DOM node. Register the class in the editors prop
on <RevoGrid> under a string key, then reference that key in the
target column’s editor field. The grid handles focus management,
keyboard navigation (Enter to commit, Escape to cancel), and calling your lifecycle
methods at the right moments.
