Handle large datasets with virtualization
react-datasheet-grid uses virtualization for both rows and columns, ensuring that display performance remains consistent regardless of the total number of rows or columns (e.g., supporting 100,000+ rows).
website·Indexed 29 days ago
https://react-datasheet-grid.netlify.app/A feature-rich, column-based data grid for React with virtualization supporting up to 100,000 rows. It provides tools for custom cell components, internationalization, and TypeScript support, with a focus on performance through static and dynamic rendering options.
.dsg-.DataSheetGridRef type and pass it to the DataSheetGrid component. This allows you to trigger grid actions (like changing the active cell or selection) from callbacks or effects.import {
DataSheetGrid,
DataSheetGridRef,
} from 'react-datasheet-grid'
import { useRef, useEffect } from 'react'
function App() {
const ref = useRef<DataSheetGridRef>(null)
useEffect(() => {
// Set an initial selection range
ref.current?.setSelection({
min: { col: 'firstName', row: 0 },
max: { col: 2, row: 3 },
})
}, [])
return (
<>
<DataSheetGrid ref={ref} />
<button onClick={() => ref.current?.setActiveCell({ col: 1, row: 0 })}>
Focus Cell
</button>
</>
)
}react-datasheet-grid library is written in TypeScript and exports all necessary types. To ensure type safety, define a type for your row data and apply it to your data state and the Column array. Use the keyColumn helper with generic type parameters to link column definitions to specific keys of your row type.import {
checkboxColumn,
Column,
DataSheetGrid,
keyColumn,
textColumn,
} from 'react-datasheet-grid'
import 'react-datasheet-grid/dist/style.css'
// Define your row type
type Row = {
active: boolean
firstName: string | null
lastName: string | null
}
function App() {
// Type your data state
const [data, setData] = useState<Row[]>([
{ active: true, firstName: 'Elon', lastName: 'Musk' },
{ active: false, firstName: 'Jeff', lastName: 'Bezos' },
])
// Type your columns to get IDE type checks
const columns: Column<Row>[] = [
{
...keyColumn<Row, 'active'>('active', checkboxColumn),
title: 'Active',
},
{
...keyColumn<Row, 'firstName'>('firstName', textColumn),
title: 'First name',
},
{
...keyColumn<Row, 'lastName'>('lastName', textColumn),
title: 'Last name',
},
]
return (
<DataSheetGrid
value={data}
onChange={setData}
columns={columns}
/>
)
}keyColumn to associate a column with a specific key in the row object. This improves performance and allows the column to operate on a specific value rather than the whole row object. You can override column properties (like title) by spreading the result of keyColumn.import { DataSheetGrid, checkboxColumn, textColumn, keyColumn } from 'react-datasheet-grid'
const Example = () => {
const [data, setData] = useState([
{ active: true, firstName: 'Elon', lastName: 'Musk' },
{ active: false, firstName: 'Jeff', lastName: 'Bezos' },
])
const columns = [
{ ...keyColumn('active', checkboxColumn), title: 'Active' },
{ ...keyColumn('firstName', textColumn), title: 'First name' },
{ ...keyColumn('lastName', textColumn), title: 'Last name' },
]
return <DataSheetGrid value={data} onChange={setData} columns={columns} />
}rowData to the select's value and use setRowData to update the state. Because some select libraries (like react-select) trigger events too early, wrap the stopEditing call in a setTimeout to ensure the grid closes the editor at the correct time.const SelectComponent = ({ rowData, setRowData, stopEditing }: CellProps) => {
return (
<Select
value={choices.find(({ value }) => value === rowData) ?? null}
options={choices}
onChange={({ value }) => {
setRowData(value);
// Delay stopEditing to avoid race conditions with react-select
setTimeout(stopEditing, 0);
}}
/>
);
};To optimize the user experience of a select column:
menuIsOpen prop to the cell's focus state to remove the need for multiple clicks.menuPortalTarget={document.body} to render the menu in a portal, ensuring it isn't cut off by the grid's scrollable area.ref and useLayoutEffect to programmatically call .focus() and .blur() on the select input when the cell gains or loses focus, enabling immediate typing/searching.import React, { useLayoutEffect, useRef } from 'react'
import Select from 'react-select'
import { CellProps } from 'react-datasheet-grid'
const SelectComponent = ({ focus }: CellProps) => {
const ref = useRef<Select>(null)
useLayoutEffect(() => {
if (focus) {
ref.current?.focus()
} else {
ref.current?.blur()
}
}, [focus])
return (
<Select
ref={ref}
menuIsOpen={focus}
menuPortalTarget={document.body}
// ... other props
/>
)
}react-datasheet-grid package using npm or yarn to begin using the data grid component in your React project.npm i react-datasheet-gridcreateTextColumn, you can provide a custom React component via the component property in the column object.const columns = [
{
component: MyCustomWidget,
title: 'My Custom Column'
}
]React.memo(). This ensures components only re-render when their props change. Note that for extremely lightweight components, a re-render may be faster than the props check performed by React.memo().const MyComponent = React.memo(({ rowData, setRowData }) => {
return <input {/*...*/} />
})
const column = { component: MyComponent, /*...*/ }DELETE operations, the indices in the operation refer to the current state (data), not the newValue. To keep deleted rows visible (e.g., highlighted in red) until a final commit, you must manually splice the deleted row back into the newValue array. If a deleted row was previously marked as 'created', it should simply be removed from the created set without being added to the deleted set.if (operation.type === 'DELETE') {
let keptRows = 0
data
.slice(operation.fromRowIndex, operation.toRowIndex)
.forEach(({ id }, i) => {
updatedRowIds.delete(id)
if (createdRowIds.has(id)) {
createdRowIds.delete(id)
} else {
deletedRowIds.add(id)
// Re-insert into newValue to keep it visible for styling
newValue.splice(
operation.fromRowIndex + keptRows++,
0,
data[operation.fromRowIndex + i]
)
}
})
}onChange handler must map the flat row indices back to the original nested data structure. Use the groupIndex and childIndex stored on the row objects to identify which specific child in the nested groups array needs to be updated, created, or deleted based on the operation type (UPDATE, CREATE, or DELETE).const handleChange = (newRows, operations) => {
for (const operation of operations) {
if (operation.type === 'UPDATE') {
for (const row of newRows.slice(operation.fromRowIndex, operation.toRowIndex)) {
if (row.type === 'CHILD') {
groups[row.groupIndex].children[row.childIndex] = { ...row };
}
}
}
if (operation.type === 'DELETE') {
const deletedRows = rows.slice(operation.fromRowIndex, operation.toRowIndex).reverse();
for (const deletedRow of deletedRows) {
if (deletedRow.type === 'CHILD') {
groups[deletedRow.groupIndex].children.splice(deletedRow.childIndex, 1);
}
}
}
// Handle CREATE similarly by finding the parent groupIndex
}
setGroups([...groups]);
};