RevoGrid in React: Building a High-Performance Spreadsheet from Scratch
There’s a particular kind of developer suffering that comes from building spreadsheet-like UIs in React. You start with a simple requirements doc — “just show some rows and columns” — and six sprints later you’re deep in DOM performance profiling, custom cell renderer archaeology, and a growing suspicion that maybe Excel wasn’t such a bad idea after all.
RevoGrid is the library that short-circuits most of that pain. This article walks you through everything: installation, configuration, virtualization mechanics, custom editors, and the kind of advanced setup that separates a toy grid from a production-grade React enterprise spreadsheet.
What Is RevoGrid and Why Does It Belong in Your React Stack?
RevoGrid is an open-source, framework-agnostic data grid built on top of Web Components (Stencil.js under the hood). It renders at native speed, handles millions of rows through virtual scrolling, and integrates cleanly into React, Vue, Angular, or plain HTML without carrying framework-specific baggage. Think of it as the spreadsheet component that doesn’t care what JavaScript religion you practice.
Where most React spreadsheet libraries bolt on virtualization as an afterthought, RevoGrid was architected with it from day one. Rows and columns are both virtualized independently, meaning a 10,000-column by 1,000,000-row grid doesn’t just technically work — it scrolls smoothly at 60fps on mid-range hardware. That’s not marketing copy; that’s the direct consequence of never rendering what’s outside the viewport.
The Web Components foundation is worth dwelling on. Because <revo-grid> is a standard custom element, it doesn’t tie you to React’s rendering cycle in the ways that pure-React grids do. You pass data in via props, listen for events via callbacks, and the grid manages its own internal DOM with surgical precision. This architecture is exactly why teams migrating away from Angular or Vue can carry their RevoGrid configuration files almost untouched — the API surface doesn’t change per framework.
RevoGrid Installation and Project Setup in React
Getting RevoGrid installed is refreshingly undramatic. The library ships a dedicated React wrapper — @revolist/react-datagrid — that wraps the core Web Component and exposes typed React props. No manual custom element registration, no document.createElement gymnastics.
# npm
npm install @revolist/react-datagrid @revolist/revogrid
# yarn
yarn add @revolist/react-datagrid @revolist/revogrid
# pnpm
pnpm add @revolist/react-datagrid @revolist/revogrid
The two-package split is intentional: @revolist/revogrid is the framework-agnostic core (the actual Web Component), while @revolist/react-datagrid is the thin React adapter layer. If you’re building a monorepo where a Vue app and a React app share the same grid instance, you install the core once and each framework adapter separately. Tidy.
Once installed, a minimal working grid in React looks like this:
import React from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
const columns = [
{ prop: 'id', name: 'ID', size: 60 },
{ prop: 'name', name: 'Name', size: 200 },
{ prop: 'email', name: 'Email', size: 260 },
];
const source = Array.from({ length: 10_000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
}));
export default function App() {
return (
<div style={{ height: '600px' }}>
<RevoGrid columns={columns} source={source} />
</div>
);
}
Two things to note immediately. First, wrap the grid in a container with an explicit height — RevoGrid fills 100% of its parent, and a parent with no height means a grid with no height, which is the most common “why is nothing showing?” moment in every tutorial. Second, those 10,000 rows in the example aren’t there to be dramatic; they’re there to demonstrate that the grid renders instantly because it never touches the DOM for rows outside the viewport.
Understanding RevoGrid Virtualization in React
Virtual scrolling is the single most important concept to understand if you’re evaluating React virtualized spreadsheet solutions. The naive approach to displaying 100,000 rows is to render 100,000 <tr> elements and let the browser handle it. The browser will handle it — by consuming 2GB of RAM, grinding the main thread to a halt, and teaching your users a new appreciation for patience. RevoGrid takes a different approach: it renders a fixed window of DOM nodes, repositions them mathematically as the user scrolls, and swaps in new data. The DOM stays small; the illusion stays complete.
What separates RevoGrid’s virtualization from libraries like react-window or react-virtual is that it virtualizes both axes simultaneously. Horizontal virtualization — across columns — is genuinely hard to implement correctly, especially when you add frozen columns, resizable headers, and merged cells into the mix. RevoGrid handles all of this natively. If you’ve ever tried to bolt column virtualization onto an existing row-virtual grid yourself, you know exactly why this matters.
From a React data grid performance standpoint, this also means you’re not fighting React’s reconciliation algorithm on large datasets. The internal DOM updates happen inside the Web Component’s shadow-like rendering context, not inside React’s virtual DOM. React sees a single <revo-grid> element with props; all the cell-level churn happens below that boundary, completely invisible to React’s diffing engine. For enterprise apps moving hundreds of thousands of cells, this boundary is the difference between a fluid product and a slideshow.
Column Configuration and Data Types
Columns in RevoGrid are defined as plain JavaScript objects. Each column object can carry a rich set of properties: prop (the data key), name (the header label), size and minSize for layout, sortable, filter, readonly, and cellTemplate or editor for custom rendering and editing behavior. The column definition is the central control plane for everything the grid does to a given field.
const columns = [
{
prop: 'status',
name: 'Status',
size: 120,
sortable: true,
filter: true,
cellTemplate: (createElement, props) => {
const color = props.model.status === 'active' ? '#22c55e' : '#ef4444';
return createElement('span', {
style: `color: ${color}; font-weight: 600`
}, props.model.status);
},
},
{
prop: 'revenue',
name: 'Revenue',
size: 140,
columnType: 'numeric',
cellTemplate: (createElement, props) => {
return createElement('span', {},
`$${Number(props.model.revenue).toLocaleString()}`
);
},
},
];
The cellTemplate function uses a createElement helper that mirrors the standard DOM API rather than JSX. This is a deliberate design choice — because the grid operates inside a Web Component context, it uses framework-agnostic rendering to avoid coupling the grid’s internals to React’s runtime. In practice, it’s slightly more verbose than JSX but entirely predictable once you’ve written two or three templates.
Column pinning (freezing) is handled via the pin property: set it to 'colPinStart' to pin a column to the left, or 'colPinEnd' to pin to the right. Frozen columns maintain their position during horizontal scrolling and are rendered in a separate virtualized layer, so pinning 5 columns in a 200-column sheet doesn’t degrade scroll performance. This is the kind of detail that distinguishes a mature React data grid from one that technically supports a feature but punishes you for using it at scale.
Implementing Custom Cell Editors in RevoGrid
This is where most grid tutorials quietly give up and point you at the documentation. Custom editors in RevoGrid are genuinely powerful, but they require understanding the editor lifecycle: the grid calls connect() when the editor mounts, getValue() when it needs to read the current value, and setValue() when it wants to set one. You implement these three methods, register the editor class under a string key, and reference that key from your column definition. That’s the entire contract.
// dropdownEditor.js
export class DropdownEditor {
element = null;
editCell = null;
connect(el, save, close) {
this.element = el;
this.editCell = { save, close };
const select = document.createElement('select');
select.style.width = '100%';
select.style.height = '100%';
select.style.border = 'none';
select.style.outline = 'none';
select.style.fontSize = '14px';
['Active', 'Inactive', 'Pending'].forEach(opt => {
const option = document.createElement('option');
option.value = opt.toLowerCase();
option.textContent = opt;
select.appendChild(option);
});
select.addEventListener('change', () => {
save(select.value);
close();
});
el.appendChild(select);
select.focus();
}
disconnectedCallback() {
// cleanup if needed
}
getValue() {
return this.element?.querySelector('select')?.value ?? '';
}
}
// App.jsx
import React from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { DropdownEditor } from './dropdownEditor';
const editors = { 'dropdown': DropdownEditor };
const columns = [
{ prop: 'name', name: 'Name', size: 200 },
{
prop: 'status',
name: 'Status',
size: 140,
editor: 'dropdown', // references the registered key
},
];
const source = [
{ name: 'Alice', status: 'active' },
{ name: 'Bob', status: 'inactive' },
{ name: 'Carol', status: 'pending' },
];
export default function App() {
return (
<div style={{ height: '400px' }}>
<RevoGrid
columns={columns}
source={source}
editors={editors}
editable
/>
</div>
);
}
The editor’s connect() method receives three arguments: the DOM element it should render into, a save callback to commit the value, and a close callback to dismiss the editor without saving. This pattern gives you complete control over when values propagate — useful for async validation, multi-step input flows, or editors that need to fetch remote options before rendering. A custom date-picker, a color-swatch selector, a searchable autocomplete — all implementable through this interface without any hacks or monkey-patching.
One subtlety worth noting: editors render into the grid’s internal DOM, not into React’s component tree. This means React hooks and context are not available inside the editor class. If you need to feed dynamic data (like a list of options from an API) into a custom editor, the cleanest pattern is to close over that data when registering the editor — pass the options as a closure variable rather than trying to reach back into React state from inside the editor class. It feels slightly unusual the first time, but it’s actually a cleaner separation of concerns than it initially appears.
Advanced Configuration: Sorting, Filtering, and Event Handling
Sorting in RevoGrid is enabled per-column via sortable: true and fires a beforesorting event that you can intercept to implement server-side sorting logic. The grid can handle client-side sorting internally with zero additional code, but for enterprise data grids where the dataset lives entirely on the server, you intercept the event, issue a new API request with the sort parameters, and update the source prop. The grid re-renders the new data efficiently via its virtual layer — no page reload, no full re-render cascade through React.
function EnterpriseGrid() {
const [data, setData] = React.useState(initialData);
const handleBeforeSorting = React.useCallback(async (event) => {
// Prevent default client-side sort
event.preventDefault?.();
const { prop, order } = event.detail;
const sorted = await fetchSortedData({ sortBy: prop, order });
setData(sorted);
}, []);
return (
<div style={{ height: '600px' }}>
<RevoGrid
columns={columns}
source={data}
editable
filter
onBeforesorting={handleBeforeSorting}
/>
</div>
);
}
Filtering follows the same pattern: enable it at the column level with filter: true, and RevoGrid renders a filter UI in the column header automatically. For client-side workloads this works out of the box. For server-side filtering, intercept the beforefiltertrimmed event, call your API, and swap the source. The ergonomics are consistent throughout — there’s no special “server mode” to switch into; you just decide how much of the data logic you own.
Event handling more broadly deserves a brief mention because it’s where the Web Component nature of RevoGrid becomes most visible in React code. Events are dispatched as native DOM custom events. The React adapter exposes them as camelCased callback props (onBeforesorting, onAfteredit, onBeforecellfocus), which aligns with React’s synthetic event model. The event.detail object contains the payload — the row model, the column definition, the new value — and TypeScript types are included, so you get full intellisense on those detail objects without any extra configuration.
RevoGrid as a Web Component: Framework Interoperability
Using Web Components as the foundation for a data grid is an architectural choice that pays dividends over time. Teams running micro-frontends — where a React shell might embed Vue or Angular sub-apps — can share a single <revo-grid> instance across boundaries without framework version conflicts. The grid doesn’t care what’s around it. It speaks HTML, properties, and custom events: the lingua franca of the browser.
In a React context, the most practical implication is that RevoGrid doesn’t inflate your React bundle with grid-specific rendering logic. The Web Component is loaded independently — often directly from a CDN in legacy applications — while React handles your application state. If you’re incrementally modernizing an older codebase, you can drop <revo-grid> into a non-React page as a custom element (<revo-grid> tag in plain HTML with a <script> module import) and then migrate it to the React wrapper later without touching any grid configuration.
There is one gotcha specific to React’s interaction with Web Components that’s worth flagging: React 17 and below have limited support for custom element properties and events — they treat custom elements roughly like unknown DOM elements and don’t correctly pass non-string props. The @revolist/react-datagrid wrapper handles this transparently, but if for any reason you’re using the raw <revo-grid> element without the wrapper in a React 17 project, you’ll need to use a ref to set complex props imperatively. React 18+ and the upcoming React 19 ship with improved custom element support that eliminates this need.
Building an Enterprise-Grade Spreadsheet: Putting It All Together
An enterprise React spreadsheet typically combines several RevoGrid features simultaneously: editable cells, custom editors for specific column types, server-driven sorting and filtering, row selection, clipboard support, and conditional formatting via cell templates. The following is a realistic skeleton of what a production component looks like — not a toy example, but not an overwhelming wall of code either.
import React, { useState, useCallback, useRef } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { DropdownEditor } from './editors/DropdownEditor';
import { DatePickerEditor } from './editors/DatePickerEditor';
const editors = {
dropdown: DropdownEditor,
datepicker: DatePickerEditor,
};
const columns = [
{ prop: 'id', name: '#', size: 55, readonly: true, pin: 'colPinStart' },
{ prop: 'account', name: 'Account', size: 220, sortable: true, filter: true },
{ prop: 'owner', name: 'Owner', size: 180, sortable: true },
{ prop: 'status', name: 'Status', size: 130, editor: 'dropdown', filter: true },
{ prop: 'dueDate', name: 'Due Date', size: 150, editor: 'datepicker' },
{ prop: 'revenue', name: 'Revenue', size: 140, columnType: 'numeric', sortable: true },
];
export function EnterpriseDataGrid({ fetchData }) {
const [source, setSource] = useState([]);
const [loading, setLoading] = useState(false);
const gridRef = useRef(null);
// Initial load
React.useEffect(() => {
setLoading(true);
fetchData({}).then(rows => {
setSource(rows);
setLoading(false);
});
}, [fetchData]);
// After a cell is edited — persist the change
const handleAfterEdit = useCallback((event) => {
const { prop, val, rowIndex, model } = event.detail;
// Optimistically update local state
setSource(prev => {
const next = [...prev];
next[rowIndex] = { ...next[rowIndex], [prop]: val };
return next;
});
// Persist to backend
fetch('/api/records/' + model.id, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [prop]: val }),
}).catch(err => {
console.error('Save failed', err);
// Roll back on error — re-fetch or revert optimistic update
});
}, []);
// Server-side sort
const handleBeforeSorting = useCallback(async (event) => {
event.preventDefault?.();
const { prop, order } = event.detail;
setLoading(true);
const rows = await fetchData({ sortBy: prop, order });
setSource(rows);
setLoading(false);
}, [fetchData]);
return (
<div style={{ height: '100%', opacity: loading ? 0.6 : 1, transition: 'opacity .2s' }}>
<RevoGrid
ref={gridRef}
columns={columns}
source={source}
editors={editors}
editable
filter
range
resize
theme="material"
onAfteredit={handleAfterEdit}
onBeforesorting={handleBeforeSorting}
/>
</div>
);
}
A few props in that example warrant explanation. range enables Excel-style range selection — the user can click-drag to select multiple cells, copy them to the clipboard, and paste a matching range elsewhere. resize makes columns resizable by dragging the column header separator. theme="material" switches to the Material-style visual theme; RevoGrid ships with default, material, compact, and darkMaterial out of the box, and you can override any CSS custom property (--revo-*) for full white-labeling.
The optimistic update pattern in handleAfterEdit deserves a note. RevoGrid fires afteredit once the user confirms a cell edit (pressing Enter or clicking outside). At that point you update local state immediately for responsiveness, then fire the network request. If the request fails, you roll back. This keeps the UI feeling instant while staying consistent with the backend — a pattern that’s equally applicable whether you’re using REST, GraphQL, or a WebSocket-based real-time sync layer.
Performance Tuning and Common Pitfalls
The most common performance mistake with RevoGrid in React is recreating the columns array or editors object on every render. Both should be defined outside the component or stabilized with useMemo. When React re-renders the parent component and passes a new array reference for columns, the grid has no choice but to treat it as a configuration change and re-initialize, which wipes any in-progress user edits and causes a visible flicker. It’s the classic referential equality trap, and it bites people every single time.
- Define
columnsandeditorsoutside the component body, or wrap them inuseMemowith a stable dependency array. - Use
useCallbackfor all event handlers passed as props to avoid triggering grid re-initialization on parent re-renders. - For very large source arrays (100k+ rows), consider passing a
trimmedRowsprop to hide rows without re-slicing the source array — this is faster than filtering the array externally and reassigning. - Use the
frameSizeprop to control how many extra rows beyond the visible viewport RevoGrid pre-renders as a scroll buffer. The default is usually fine, but increasing it slightly on slower machines eliminates white-flash during fast scrolling.
Memory management is largely handled for you, but if you’re dynamically swapping entire datasets (switching between two completely different data views), call gridRef.current.clearFocus() before updating the source to prevent stale selection state from pointing at rows that no longer exist. This is a minor edge case but shows up in dashboards where the same grid component displays different reports based on user navigation.
TypeScript integration is first-class. All RevoGrid types are exported from @revolist/revogrid — ColumnRegular, DataType, EditorBase, RevoGrid component props — so you can type your columns, source data, and editor classes fully without any @ts-ignore workarounds. If you’re starting a new project, scaffolding it in TypeScript from the beginning is the path of least resistance; the type errors catch configuration mistakes early, before they become runtime surprises in production.
Frequently Asked Questions
How does RevoGrid handle large datasets in React?
RevoGrid uses bidirectional virtual scrolling — both rows and columns are virtualized independently. The DOM always contains only the cells currently visible in the viewport plus a small configurable buffer. A grid with 1,000,000 rows and 500 columns renders at the same speed as one with 100 rows, because the underlying DOM complexity never changes. This makes it one of the most capable React virtualized spreadsheet solutions available without requiring any application-level pagination logic.
How do I create custom cell editors in RevoGrid?
You implement a class with a connect(el, save, close) method that renders your editor into the provided DOM element and uses the save callback to commit values. Optionally implement getValue() and a cleanup handler. Register the class in an editors object with a string key, pass that object to the editors prop on <RevoGrid>, and reference the string key in your column definition via the editor property. The full example in this article demonstrates a working dropdown editor.
Is RevoGrid production-ready for enterprise React applications?
Yes. RevoGrid is actively maintained (v4+ as of 2025), ships with full TypeScript support, and is designed explicitly for enterprise workloads. It supports column pinning, range selection, clipboard integration, server-side sorting and filtering, custom renderers and editors, multiple themes, and accessibility features. Its Web Component architecture means it’s not tied to React’s release cycle, so breaking changes in React do not break the grid — a meaningful stability advantage for long-running enterprise projects.
Final Thoughts
RevoGrid sits in a comfortable place in the ecosystem: genuinely performant, framework-agnostic, and feature-complete enough for enterprise use without the licensing cost or API complexity of AG Grid Enterprise. The revo-grid React integration is clean, the TypeScript types are thorough, and the custom editor system — while slightly unusual due to its Web Component context — gives you the flexibility to build any editing UI imaginable.
If you’re evaluating React spreadsheet libraries for a new project, the fastest path to an informed decision is to prototype your hardest use case first: the weirdest column type, the largest dataset, the most complex custom editor. RevoGrid handles all three without breaking a sweat. For projects already using it, the advanced configuration patterns in this article — server-side sort/filter, optimistic edits, TypeScript-typed columns — should give you a solid foundation for whatever comes next.
For a deeper dive into real-world implementation patterns, the original source that informed parts of this article is worth reading:
Advanced Spreadsheet Implementation with RevoGrid in React — dev.to/stackforgetx.


Recent Comments