Back to Blog
· 6 min read

协同表格 react-data-grid 的基本框架

React

协同表格需要解决:多种编辑器类型、复杂的编辑状态管理、单元格之间的联动、键盘导航、粘贴复制、性能优化……本文以 react-data-grid 为例,介绍可协同表格的架构设计与技术要点。

react-data-grid 是 Comcast 开源的 React 表格库,被众多企业用于生产环境,它的设计思路对理解协同表格很有帮助。

协同表格的技术挑战

在分析 react-data-grid 之前,先看看做一个协同表格需要面对哪些问题:

编辑器复杂性

挑战说明
多种编辑器类型文本、数字、日期、下拉、多选、关联……
编辑状态切换点击编辑 → 失去焦点保存 / 手动保存
编辑器定位单元格内编辑 vs 弹出层编辑
验证逻辑每个编辑器有自己的验证规则

状态管理

挑战说明
编辑状态哪个单元格正在编辑?
选中状态哪个单元格被选中?
行/列变化新增行、删除列
撤销/重做每次编辑需要记录历史

用户交互

挑战说明
键盘导航方向键切换单元格、Tab 跳转
快捷键Ctrl+C 复制、Ctrl+V 粘贴
拖拽拖拽填充、拖拽调整列宽
多选Shift 点击、Ctrl 点击

性能问题

挑战说明
大数据量十万级数据如何渲染?
频繁重渲染编辑状态变化如何避免整表刷新?
虚拟滚动只渲染可视区域的行和列

react-data-grid 基本架构

设计理念

react-data-grid 的设计围绕几个核心原则:

  1. 配置驱动:通过列定义配置所有行为
  2. 渲染器模式:用自定义组件处理渲染和编辑
  3. 统一状态管理:编辑状态由 DataGrid 统一管理
  4. 虚拟化:高性能渲染大量数据

组件层级

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]

数据流

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

核心模式:Renderers

什么是 Renderer

Renderer 是 react-data-grid 的核心概念:用自定义组件处理单元格的渲染和编辑

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

这种方式把”怎么显示”和”怎么编辑”完全交给开发者,库本身只负责框架。

Renderer 的分类

类型用途接口
cellRenderer单元格渲染{ row, column, isCellSelected }
headerRenderer表头渲染{ column, sortDirection }
rowRenderer行渲染{ row, renderers }
summaryRowRenderer汇总行渲染{ row, isCellSelected }

CellRenderer 示例

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

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

为什么用 renderer 模式?

优点

  • 灵活性高:任何 React 组件都可以作为 renderer
  • 可复用:同一个 renderer 可以用于不同列
  • 关注点分离:显示逻辑和表格逻辑解耦

缺点

  • 需要开发者理解接口
  • 过度灵活可能导致不一致

编辑器模式

EditorContainer

react-data-grid 使用 EditorContainer 统一管理编辑状态:

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 });
  }
}

编辑器接口

react-data-grid 定义了编辑器的标准接口:

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

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

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

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

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

内置编辑器

react-data-grid 提供了一些内置编辑器:

编辑器用途
AutoComplete自动补全
DropDownEditor下拉选择
DateEditor日期选择

自定义编辑器示例

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())}
      />
    );
  }
}

编辑器的难点

难点解决方案
编辑器与单元格位置绝对定位、z-index 管理
编辑器生命周期onFocus、onBlur、onCommit
验证失败处理显示错误状态、阻止提交
键盘事件处理方向键、Tab、Enter、Escape

老生常谈的状态管理

DataGrid 状态

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

状态更新模式

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

编辑状态的传递

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

状态管理的痛点

痛点说明
编辑与选中的冲突正在编辑时还能选中其他单元格吗?
批量编辑复制粘贴多行数据如何处理?
撤销重做如何设计历史栈?
远程数据同步编辑后如何与服务器同步?

键盘导航

导航逻辑

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);
    }
    // ...
  }
};

Tab 导航的难点

难点说明
循环跳转到最后一列后回到第一列?
跨行编辑完一行后自动跳到下一行?
跳过不可编辑列如何配置哪些列可编辑?
编辑中提交Tab 时自动提交还是取消?

虚拟滚动

为什么需要虚拟滚动

当数据量达到数万行时,一次性渲染所有 DOM 会导致严重性能问题。

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>
    );
  }
}

虚拟化的挑战

挑战说明
行高不确定动态行高如何处理?
滚动位置同步虚拟滚动与实际滚动如何同步?
粘性列固定列如何在虚拟滚动中保持?
性能滚动时如何避免频繁计算?

类似产品

特色

产品特色
Airtable丰富的字段类型、视图、自动化
Notion块式编辑、数据库关联
Miro无限画布、实时协作

差异

react-data-gridAirtable/Notion
实时协作WebSocket/CRDT
字段类型基础丰富(关联、公式等)
视图切换多视图(看板、日历等)
离线支持有限

实时协作

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

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

    E --> F[CRDT 算法]
    F --> G[冲突解决]
    G --> H[同步状态]
  • CRDT:无冲突复制数据类型,用于解决并发编辑
  • OT:操作转换,另一个主流方案
  • WebSocket:实时通信

总结

react-data-grid 的架构亮点

  1. Renderer 模式:灵活性与结构性的平衡
  2. EditorContainer:统一的编辑状态管理
  3. 虚拟滚动:解决大数据量性能问题
  4. 键盘导航:完整的无障碍支持

协同表格的进阶挑战

挑战难度说明
多种编辑器类型配置化可解决
编辑状态管理状态机可解决
键盘导航需要完善的事件处理
虚拟滚动需要精确的位置计算
实时协作极高需要 CRDT/OT 等复杂算法

延伸阅读