React Data Grid: Complete Guide to Building Interactive Tables in 2025
Why react-data-grid Still Wins for Complex Tables
Every React developer eventually hits the same wall: <table> works fine until your product manager asks for
editable cells, column sorting, keyboard navigation, and “oh, can it handle 50,000 rows without lagging?”
That’s precisely the moment react-data-grid
— originally built by Adazzle and now one of the most starred
React data grid libraries on npm —
stops being optional and starts being inevitable.
Unlike heavyweight enterprise options such as AG Grid or Handsontable,
react-data-grid
hits a sweet spot: it is genuinely open-source (MIT), ships with virtual scrolling out of the box,
requires zero configuration to render 100k rows smoothly, and keeps its API surface area small enough
that you can actually read the documentation in one sitting. The library handles the hard parts —
DOM virtualization, cell focus management, clipboard operations — while leaving you in control of
data, styling, and business logic.
This guide covers everything from first installation to advanced patterns:
sorting, filtering, inline editing, custom cell renderers, and row selection.
All examples use react-data-grid v7, which introduced a significantly cleaner
prop API and first-class TypeScript support. If you have been reading tutorials based on v6
and wondering why nothing compiles, that is your answer.
Installing and Setting Up react-data-grid
React data grid installation
is refreshingly boring, which is exactly what you want when you’re already juggling a complex UI.
The package has no peer dependency drama beyond React 18+ and does not need a separate CSS-in-JS solution.
# npm
npm install react-data-grid
# yarn
yarn add react-data-grid
# pnpm
pnpm add react-data-grid
After installing, import both the component and its base stylesheet.
The stylesheet is intentionally minimal — it handles cell borders, focus rings,
and the header row, but leaves fonts, background colors, and spacing open for your own design system.
import DataGrid from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
The component accepts two required props: columns — an array of column definitions —
and rows — your data array. That’s the entire surface area for a basic
React interactive table.
Everything else — sorting, editing, row height, custom renderers — is opt-in.
No 200-option config object to decode before you can render “Hello World.”
Your First react-data-grid Example
Let’s build a minimal but real example: a product catalog table with three columns.
This is the pattern you’ll extend for 90% of use cases, so it’s worth getting the mental model right
before reaching for advanced features.
import { useState } from 'react';
import DataGrid from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
const columns = [
{ key: 'id', name: 'ID', width: 80 },
{ key: 'product', name: 'Product', width: 200 },
{ key: 'price', name: 'Price', width: 120 },
];
const initialRows = [
{ id: 1, product: 'Wireless Mouse', price: '$29.99' },
{ id: 2, product: 'Mechanical Keyboard', price: '$89.99' },
{ id: 3, product: 'USB-C Hub', price: '$45.00' },
];
export default function ProductGrid() {
const [rows, setRows] = useState(initialRows);
return (
<DataGrid
columns={columns}
rows={rows}
onRowsChange={setRows}
style={{ height: 350 }}
/>
);
}
The onRowsChange callback already wires up the state management pattern
you will use for editing — pass the setter directly, and the grid handles diffing internally.
Notice that style={{ height: 350 }} is not optional cosmetics:
react-data-grid requires an explicit height to calculate its virtual scroll viewport.
Omit it and you’ll get a zero-height container with a mildly haunting debugging session ahead.
Column width is optional but recommended for initial render performance.
Without it, the library distributes space equally — which works but can look odd with
columns of wildly different content lengths. You can also set minWidth,
maxWidth, and resizable: true to give users control over column sizing
without writing a single resize handler yourself.
This is the base of every React data table
pattern you’ll build from here. Once this renders correctly, you own the loop:
modify columns for behavior, modify rows for data,
and intercept onRowsChange for persistence.
Sorting: Making Columns Actually Useful
react-data-grid does not sort rows for you — it tells you what the user wants and trusts you
to sort your own data. This is the right call. Your rows might come from a server,
a Redux store, or a local array; the library has no business deciding your sort algorithm.
What it does handle is all the UI state: sort direction indicators, column header interactions,
and multi-column sort tracking.
import { useState, useMemo } from 'react';
import DataGrid from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
const columns = [
{ key: 'product', name: 'Product', sortable: true },
{ key: 'price', name: 'Price', sortable: true },
{ key: 'stock', name: 'In Stock', sortable: true },
];
const rawRows = [
{ id: 1, product: 'Wireless Mouse', price: 29.99, stock: 142 },
{ id: 2, product: 'Mechanical Keyboard', price: 89.99, stock: 38 },
{ id: 3, product: 'USB-C Hub', price: 45.00, stock: 217 },
{ id: 4, product: 'Monitor Stand', price: 55.00, stock: 0 },
];
export default function SortableGrid() {
const [sortColumns, setSortColumns] = useState([]);
const sortedRows = useMemo(() => {
if (sortColumns.length === 0) return rawRows;
return [...rawRows].sort((a, b) => {
for (const { columnKey, direction } of sortColumns) {
const order = a[columnKey] > b[columnKey] ? 1 : -1;
if (order !== 0) return direction === 'ASC' ? order : -order;
}
return 0;
});
}, [sortColumns]);
return (
<DataGrid
columns={columns}
rows={sortedRows}
sortColumns={sortColumns}
onSortColumnsChange={setSortColumns}
style={{ height: 300 }}
/>
);
}
The useMemo wrapper is important here. Sorting inside render without memoization
will fire on every keystroke anywhere in your app, making large datasets visibly sluggish.
Wrap your sort logic and list sortColumns as the dependency —
the grid only re-sorts when the user actually changes the sort configuration.
Multi-column sorting works out of the box: holding Ctrl (or Cmd on Mac)
while clicking a column header adds it to the sort stack. The sortColumns array
reflects the priority order, so your comparator loop naturally handles tiebreaking.
This is something that takes non-trivial effort to implement manually in vanilla
TanStack Table
or a custom React grid component.
Filtering: The Pattern That Actually Scales
Filtering in react-data-grid follows the same philosophy as sorting:
the library gives you the infrastructure, you own the logic.
The recommended pattern uses a header row filter — a secondary row beneath column headers
containing input fields — which maps naturally to how users expect spreadsheet-style filtering to work.
import { useState, useMemo } from 'react';
import DataGrid from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
function FilterInput({ value, onChange }) {
return (
<input
style={{ width: '100%', padding: '4px', boxSizing: 'border-box' }}
value={value}
onChange={e => onChange(e.target.value)}
placeholder="Filter..."
/>
);
}
export default function FilterableGrid() {
const [filters, setFilters] = useState({ product: '', price: '' });
const rawRows = [
{ id: 1, product: 'Wireless Mouse', price: 29.99 },
{ id: 2, product: 'Mechanical Keyboard', price: 89.99 },
{ id: 3, product: 'USB-C Hub', price: 45.00 },
];
const filteredRows = useMemo(() =>
rawRows.filter(row =>
row.product.toLowerCase().includes(filters.product.toLowerCase()) &&
String(row.price).includes(filters.price)
),
[filters]
);
const columns = [
{
key: 'product',
name: 'Product',
renderHeaderCell: () => (
<FilterInput
value={filters.product}
onChange={v => setFilters(f => ({ ...f, product: v }))}
/>
)
},
{
key: 'price',
name: 'Price ($)',
renderHeaderCell: () => (
<FilterInput
value={filters.price}
onChange={v => setFilters(f => ({ ...f, price: v }))}
/>
)
},
];
return (
<DataGrid
columns={columns}
rows={filteredRows}
style={{ height: 300 }}
/>
);
}
The renderHeaderCell prop replaces the default column header with any React node.
This means your filter UI can be a plain input, a dropdown, a date picker, or a multi-select —
the grid does not care. This flexibility is what makes react-data-grid competitive with
full-featured React spreadsheet table libraries that bake in a specific filter UI
you then spend hours overriding.
For server-side filtering, simply replace the useMemo derivation with
a useEffect that fires an API call whenever filters changes,
and feed the response into your rows state. The grid itself never touches your data fetching logic —
a clean separation that becomes invaluable in large applications where data lives in React Query,
SWR, or a global store.
One practical note: debounce your filter inputs if you’re making network requests.
A 300ms debounce prevents a flood of API calls while the user is still typing.
For local filtering of arrays under ~10,000 rows, debouncing is usually unnecessary —
the useMemo computation resolves well within a single frame on modern hardware.
Inline Editing: Turning Your Grid Into a Spreadsheet
React data grid editing
is where the library genuinely earns its reputation. Double-clicking a cell —
or pressing Enter with it focused — activates the cell editor.
The simplest path uses textEditor, a built-in editor exported from the library itself.
import { useState } from 'react';
import DataGrid, { textEditor } from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
const columns = [
{ key: 'product', name: 'Product', renderEditCell: textEditor },
{ key: 'price', name: 'Price', renderEditCell: textEditor },
];
const initial = [
{ id: 1, product: 'Wireless Mouse', price: '29.99' },
{ id: 2, product: 'Mechanical Keyboard', price: '89.99' },
];
export default function EditableGrid() {
const [rows, setRows] = useState(initial);
return (
<DataGrid
columns={columns}
rows={rows}
onRowsChange={setRows}
style={{ height: 250 }}
/>
);
}
When you need something beyond a plain text input — a number spinner,
a dropdown, a date picker — you implement a custom editor component.
It receives row, column, and onRowChange as props,
renders your input, and calls onRowChange(updatedRow, true)
(the true flag commits the edit) when the user confirms their value.
The API is intentionally small: you are writing a controlled input, nothing more exotic than that.
function SelectEditor({ row, column, onRowChange }) {
return (
<select
autoFocus
value={row[column.key]}
onChange={e => onRowChange({ ...row, [column.key]: e.target.value }, true)}
style={{ width: '100%', height: '100%' }}
>
<option value="In Stock">In Stock</option>
<option value="Low Stock">Low Stock</option>
<option value="Out of Stock">Out of Stock</option>
</select>
);
}
Keyboard behavior is handled for you: Escape cancels the edit,
Tab moves to the next editable cell, and Enter commits and
moves to the cell below — exactly the behavior users expect from a
spreadsheet-like interface.
If you’ve ever implemented this from scratch, you know how many edge cases live in that
two-sentence description. react-data-grid handles all of them.
Row Selection, Custom Renderers, and Column Resizing
Production data grids inevitably need row selection for bulk operations —
delete selected, export selected, update status for selected rows.
react-data-grid provides a SelectColumn export that adds a checkbox column
with select-all behavior in the header, handling shift-click range selection automatically.
import DataGrid, { SelectColumn } from 'react-data-grid';
const columns = [
SelectColumn,
{ key: 'product', name: 'Product' },
{ key: 'price', name: 'Price' },
];
// In your component:
const [selectedRows, setSelectedRows] = useState(new Set());
<DataGrid
columns={columns}
rows={rows}
selectedRows={selectedRows}
onSelectedRowsChange={setSelectedRows}
rowKeyGetter={row => row.id}
style={{ height: 300 }}
/>
Custom cell renderers work through the renderCell prop on each column.
You receive the full row object and can return any JSX — badges, progress bars,
action buttons, sparklines. This is what separates a
React interactive table
built on react-data-grid from a plain HTML table:
your UI components live inside cells without any hacks.
Column resizing requires nothing beyond resizable: true in the column definition.
The library stores column widths internally during a session;
if you need persistence across page loads, read the widths back via onColumnResize
and store them in localStorage or user preferences.
Combined with frozen: true for pinning columns to the left edge during horizontal scroll,
you have the full toolkit for a professional-grade
React grid component.
Performance: Handling Large Datasets Without Breaking a Sweat
Virtual scrolling is not a feature you enable in react-data-grid — it is simply how the library works.
Only the rows currently visible in the viewport are mounted in the DOM,
regardless of whether your dataset has 1,000 or 500,000 rows.
This is the single most important reason to choose react-data-grid over a naive
<table> implementation for any dataset that might grow unpredictably.
To keep rendering fast on your side of the equation, memoize your column definitions
outside the component or with useMemo. Column arrays recreated on every render
cause the grid to re-measure layout unnecessarily.
Similarly, keep onRowsChange stable with useCallback
when passing it through multiple component layers —
reference equality matters for React’s bailout optimizations.
For row heights, the default is a fixed 35px which makes virtualization trivially accurate.
If you need variable row heights, use rowHeight as a function:
rowHeight={row => row.expanded ? 120 : 35}.
The grid recalculates the scroll position correctly.
Just avoid computing row height from DOM measurements — keep it derivable from data alone,
or you’ll find yourself in a render loop that no amount of coffee fixes.
react-data-grid vs. the Alternatives
The React ecosystem has no shortage of table and grid solutions.
Understanding where react-data-grid fits prevents the classic mistake of
choosing the wrong tool and spending a week fighting its abstractions.
- TanStack Table (React Table v8): Headless — no DOM, no CSS, complete UI control. Ideal when you’re building a design system or need a table that matches a highly specific visual language. The tradeoff is that you implement every interaction yourself.
- AG Grid Community: Feature-rich, battle-hardened, but the free tier is increasingly limited and the paid tier is enterprise-priced. Excellent for internal tools where budget exists; overkill for most product use cases.
- MUI Data Grid: Best choice if you’re already deep in the Material UI ecosystem. The free version (Community) covers basic use cases; advanced features like Excel export and row grouping are behind a commercial license.
- react-data-grid: The pragmatic middle ground — opinionated enough to ship fast, flexible enough to handle real product requirements, MIT licensed with no paid tier. The row virtualization is genuinely excellent.
The honest answer is that react-data-grid wins specifically when you need editable cells,
keyboard navigation, and high row counts without reaching for a paid license.
If you need pivot tables, row grouping, or tree data out of the box,
look at AG Grid or MUI Data Grid Pro first — react-data-grid does not cover those patterns natively,
though creative use of row renderers can approximate some of them.
TypeScript: First-Class, Not an Afterthought
react-data-grid v7 ships with TypeScript definitions built into the package — no @types/
package required. The Column<TRow> generic type propagates through your entire grid setup,
meaning renderCell, renderEditCell, and onRowsChange
all know the shape of your row data. Misspell a key in a column definition and your editor
catches it before the browser does.
import DataGrid, { Column } from 'react-data-grid';
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
const columns: Column<Product>[] = [
{ key: 'id', name: 'ID' },
{ key: 'name', name: 'Product' },
{ key: 'price', name: 'Price' },
{ key: 'inStock', name: 'In Stock' },
];
The rowKeyGetter prop accepts a typed function (row: TRow) => React.Key,
ensuring you return a valid key type without casting.
When you define a renderEditCell function,
its row parameter is inferred as TRow —
no more row as any to access your properties.
This level of type safety pays dividends as your column list grows.
Refactoring a row interface — renaming a field, adding a new property —
surfaces every affected column definition immediately.
For teams maintaining large React applications, this alone justifies choosing
react-data-grid over libraries that rely on stringly-typed column keys.
the article at
dev.to/stackforgetx
is worth reading alongside this guide — it covers several integration scenarios not duplicated here.
Frequently Asked Questions
How do I install react-data-grid in a React project?
Run npm install react-data-grid (or the yarn/pnpm equivalent).
Then import the component and its stylesheet at the top of your file:
import DataGrid from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
Define a columns array and a rows array,
pass them as props, set an explicit style={{ height: N }} —
and your grid is rendering. Total setup time: under five minutes.
How do I add sorting and filtering to react-data-grid?
For sorting, add sortable: true to any column definition,
then manage a sortColumns state with onSortColumnsChange.
Derive sorted rows with useMemo using a comparator that reads from sortColumns.
For filtering, use renderHeaderCell on each column to render
a controlled input. Derive filtered rows with useMemo by running
your filter logic against the current filter state. Both patterns work together cleanly —
chain your sort and filter derivations in sequence.
How do I enable inline editing in react-data-grid?
Add renderEditCell: textEditor to any column (import textEditor
from 'react-data-grid'). Pass onRowsChange={setRows} to the grid.
Users activate editing by double-clicking a cell or pressing Enter.
For custom editors (dropdowns, date pickers), write a component that receives
row, column, and onRowChange as props,
renders your input, and calls onRowChange(updatedRow, true) on commit.