React Datasheet Grid

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.

Tokens
11.4K
Snippets
57
Records
73
Agent score
75%

What's inside React Datasheet Grid

  1. 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).
  2. Control react-datasheet-grid using a ref

    To programmatically control the grid from a parent component, create a ref with the 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>
        </>
      )
    }
  3. Implement react-datasheet-grid with TypeScript

    The 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}
        />
      )
    }
  4. Map columns to object properties using keyColumn

    When rows are objects rather than primitive values, use 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} />
    }
  5. Bind data and handle changes in a Select cell component

    To ensure a select component reflects the grid's state and persists changes, you must bind the 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);
          }}
        />
      );
    };
  6. Improve select menu behavior and accessibility

    To optimize the user experience of a select column:

    1. Auto-open Menu: Bind the menuIsOpen prop to the cell's focus state to remove the need for multiple clicks.
    2. Prevent Clipping: Use menuPortalTarget={document.body} to render the menu in a portal, ensuring it isn't cut off by the grid's scrollable area.
    3. Manage Input Focus: Use a 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
        />
      )
    }
  7. Implement a column with a custom component

    For complex widgets that cannot be handled by createTextColumn, you can provide a custom React component via the component property in the column object.
    const columns = [
      {
        component: MyCustomWidget,
        title: 'My Custom Column'
      }
    ]
  8. Optimize cell rendering with React.memo()

    To prevent unnecessary re-renders in react-datasheet-grid, wrap all cell components in 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, /*...*/ }
  9. Handle row deletions with visual persistence

    For 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]
            )
          }
        })
    }
  10. Handle data changes for collapsible rows

    When using collapsible rows, the 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]);
    };