Back to Blog
By AriesZhou · · 6 min read

Collaborative Tables with react-data-grid (Part 1)

React

react-data-grid is an open-source React table library from Comcast, used by many enterprises in production. Its design approach is helpful for understanding collaborative tables.

Common interaction issues with table components

Table components, especially collaborative tables, involve complex scenarios, interactions, and edge cases. Here is a brief list of common scenarios and issues at the user interaction level:

  1. Editors

Complex types: for example, plain text, long text (rich text), numbers, dates, times, single select, multi-select, links, etc.

Edit state switching: click interactions, hover interactions, focus/blur interactions, etc.

Positioning: in-cell editing (no positioning concerns here), popup-layer editing (requires considering where to pop up and edge cases)

Validation: input type restrictions, masking, etc. Different editors have their own validation rules.

  1. State management

Edit state: which cell is currently being edited?

Selection state: which cell or cells are selected? Is it shift multi-select or drag-to-select?

Row and column changes: add/delete/reorder/drag/filter/group, etc.

Undo/redo: keyboard shortcuts, history, validity period, etc.

  1. User interaction

Keyboard navigation: arrow keys to switch selected cells, tab navigation, and other a11y issues

Keyboard shortcuts: intuitive copy, paste, select all, undo, etc.

Drag and drop: fill-down, rule definition, column resize, row dragging, etc.

Multi-select: shift-click range selection, control/command-click individual selection, mouse drag box selection, etc.

  1. Performance issues

Data loading: how to load large amounts of data?

Re-rendering: how to avoid refreshing the entire table when edit state changes?

Virtual scrolling: render only rows/columns in the visible area and buffer zone

For collaborative tables, you also need to consider data synchronization and how to update data and state in a user-friendly way in the UI. This is a fairly complex topic. For now, let’s set aside the data sync scenario and study the design and implementation of react-data-grid purely from the UI side.

react-data-grid basic architecture

Design philosophy

react-data-grid’s design revolves around several core principles:

  1. Configuration-driven: all behavior is configured through column definitions
  2. Renderer pattern: custom components handle rendering and editing
  3. Unified state management: edit state is managed centrally by DataGrid
  4. Virtualization: high-performance rendering of large datasets

Component hierarchy

flowchart TD
    A[DataGrid] --> B[HeaderRow]
    A --> C[Rows]
    A --> D[EditorContainer]
    A --> E[SummaryRow]

    C --> F[Row]
    F --> G[Cell]
    G --> H[CellRenderer]

    D --> I[Editor]
    I --> J[EditorContainer]

Data flow

flowchart LR
    A[用户交互] --> B[DataGrid]
    B --> C[更新 state]
    C --> D[触发重渲染]
    D --> E[渲染 Cell/Editor]
    E --> F[Editor 回调]
    F --> B

Core pattern: Renderers

Renderer is the core concept of react-data-grid: Use custom components to handle cell rendering and editing.

const columns = [
  {
    key: 'name',
    name: '姓名',
    renderer: <CustomCellRenderer />,
    editor: <CustomEditor />,
  }
];

This approach hands “how to display” and “how to edit” entirely to the developer, while the library itself only provides the framework.

Categories

TypePurposeInterface
cellRendererCell rendering{ row, column, isCellSelected }
headerRendererHeader rendering{ column, sortDirection }
rowRendererRow rendering{ row, renderers }
summaryRowRendererSummary row rendering{ row, isCellSelected }

Demo

// 自定义单元格渲染器
const CustomCellRenderer = ({ row, column, isCellSelected }) => {
  const value = row[column.key];

  return (
    <div
      className={`cell ${isCellSelected ? 'selected' : ''}`}
      style={{ backgroundColor: isCellSelected ? '#e3f2fd' : 'transparent' }}
    >
      {value}
    </div>
  );
};

Benefits:

  • High flexibility: any React component can serve as a renderer
  • Reusable: the same renderer can be used across different columns
  • Separation of concerns: display logic is decoupled from table logic

Risks:

  • Excessive flexibility can lead to inconsistency

Editors

EditorContainer

react-data-grid uses EditorContainer to manage editing state uniformly:

class EditorContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editing: null,  // { rowIdx, colIdx }
      value: null
    };
  }

  startEdit = ({ rowIdx, colIdx }) => {
    this.setState({
      editing: { rowIdx, colIdx },
      value: this.getCellValue(rowIdx, colIdx)
    });
  }

  commitEdit = () => {
    const { rowIdx, colIdx } = this.state.editing;
    const { onCellCommit } = this.props;

    onCellCommit({
      rowIdx,
      colIdx,
      value: this.state.value
    });

    this.setState({ editing: null });
  }
}

Interface

react-data-grid defines a standard interface for editors:

interface CellEditor {
  // 获取编辑器当前值
  getValue(): any;

  // 获取原始值(用于取消编辑)
  getOldValue(): any;

  // 验证编辑器值
  validate(): boolean;

  // 可选:焦点处理
  focus?(): void;

  // 可选:获取 props
  getProps?(): any;
}

Built-in editors

react-data-grid provides several built-in editors:

EditorPurpose
AutoCompleteAutocomplete
DropDownEditorDropdown selection
DateEditorDate selection

Custom editors

class TextEditor extends React.Component {
  getValue() {
    return {
      [this.props.column.key]: this.input.value
    };
  }

  getOldValue() {
    return {
      [this.props.column.key]: this.props.value
    };
  }

  validate() {
    return this.input.value.length > 0;
  }

  render() {
    return (
      <input
        ref={(node) => this.input = node}
        defaultValue={this.props.value}
        onBlur={() => this.props.onCommit(this.getValue())}
      />
    );
  }
}

Challenges with editors

ChallengeSolution
Editor and cell positioningAbsolute positioning, z-index management
Editor lifecycleonFocus, onBlur, onCommit
Validation failure handlingShow error state, block commit
Keyboard event handlingArrow keys, Tab, Enter, Escape

State management

class DataGrid extends React.Component {
  state = {
    rows: [],           // 数据
    selected: null,     // 选中状态 { rowIdx, colIdx }
    editing: null,     // 编辑状态 { rowIdx, colIdx }
    sortColumns: [],   // 排序列
    filters: {},       // 过滤条件
    columnWidths: {},  // 列宽
  };
}

State update patterns

// 统一的状态更新入口
updateState = (updates) => {
  this.setState(updates, () => {
    // 状态更新后的回调
    this.persistState();
  });
};

Edit state propagation

// DataGrid -> Cell -> Editor
<DataGrid>
  <Cell
    isEditing={this.state.editing?.rowIdx === rowIdx && this.state.editing?.colIdx === colIdx}
    onEdit={this.startEdit}
  />
</DataGrid>

The design of edit state management needs to consider the following points:

  • Conflict between editing and selection states: can other cells still be selected while editing is in progress, and how should clicking another cell during editing behave?
  • When supporting batch editing, how to handle copying and pasting multiple rows of data, how to handle mismatched data formats, and how to handle cases where more cells are copied than the target cells.
  • Undo functionality: how to design the history stack.

Keyboard navigation

handleKeyDown = (e) => {
  const { selected, editing } = this.state;

  if (editing) {
    // 编辑状态下的键盘处理
    if (e.key === 'Escape') {
      this.cancelEdit();
    } else if (e.key === 'Enter') {
      this.commitEdit();
    } else if (e.key === 'Tab') {
      e.preventDefault();
      this.moveToNextCell(e.shiftKey);
    }
  } else {
    // 非编辑状态的键盘处理
    if (e.key === 'ArrowDown') {
      this.moveSelection(1, 0);
    }
    // ...
  }
};

Common issues with Tab navigation

  • Circular navigation: return to the first column after reaching the last column?
  • Across rows: after finishing editing one row, should Tab automatically jump to the next row?
  • Skipping non-editable columns: how to configure which columns are editable?
  • Committing while editing: on Tab, should changes be committed automatically or cancelled?

Virtual scrolling

When the data volume reaches tens of thousands of rows, rendering all DOM nodes at once causes severe performance issues, so virtual scrolling is necessary.

Virtualization in react-data-grid

// 简化版虚拟滚动
class VirtualRows extends React.Component {
  render() {
    const { rows, scrollTop, rowHeight } = this.props;
    const startIdx = Math.floor(scrollTop / rowHeight);
    const endIdx = startIdx + VISIBLE_COUNT;

    const visibleRows = rows.slice(startIdx, endIdx);

    return (
      <div style={{ height: rows.length * rowHeight }}>
        <div style={{ transform: `translateY(${startIdx * rowHeight}px)` }}>
          {visibleRows.map((row, i) => (
            <Row
              key={row.id}
              row={row}
              index={startIdx + i}
            />
          ))}
        </div>
      </div>
    );
  }
}

Challenges of virtualization

ChallengeDescription
Uncertain row heightHow to handle dynamic row heights?
Scroll position synchronizationHow to synchronize virtual scrolling with actual scrolling?
Sticky columnsHow to keep fixed columns in place during virtual scrolling?
PerformanceHow to avoid frequent calculations while scrolling?

Collaboration

flowchart LR
    A[用户 A] --> B[本地状态]
    C[用户 B] --> D[本地状态]

    B --> E[协同服务器]
    D --> E

    E --> F[CRDT 算法]
    F --> G[冲突解决]
    G --> H[同步状态]
  • CRDT: Conflict-free Replicated Data Types for resolving concurrent edits
  • OT: Operational Transformation, another mainstream approach
  • WebSocket: Real-time communication