解决 Ant Design Table + DND 输入框焦点丢失问题

如何解决 Ant Design Table + DND 输入框焦点丢失问题。

1. 问题现象

在使用 React + Ant Design Table + react-beautiful-dnd 实现可拖拽表格时,表格中包含可编辑的 Input 输入框。

  • 现象:鼠标点击输入框,输入一个字符后,输入框立即失去焦点(Blur),必须重新点击才能继续输入。
  • 后果:无法连续输入,用户体验极差。

2. 问题原因

这是 React 的渲染机制、引用相等性检查(Referential Equality)和组件生命周期共同作用的结果。

主要原因:

  1. 输入触发更新:用户输入字符 -> setState 更新数据 -> 组件重新渲染 (Re-render)。
  2. 生成新对象react-beautiful-dnd<Droppable> 回调函数执行,生成一个新的 provided 对象(引用变了)。
  3. 配置发生变化:新的 provided 对象被传递给了 Table 的 components 属性(或者 components 本身就是每次渲染内联生成的)。
  4. Table 强制重置:Ant Design Table 检测到 components 属性的引用发生了变化。为了保证渲染正确性,它销毁了旧的 tbody DOM 节点,并重新创建新的 tbody
  5. 焦点丢失:旧的 <input> 随 DOM 销毁而消失,新的 <input> 虽然被创建,但默认没有焦点。

次要原因

  • 如果 columns 定义依赖了不稳定的函数(未用 useCallback),导致 columns 引用变化,会触发列级别的重绘,同样导致焦点丢失。

3. 解决方案

采用 “Context 穿透 + 引用冻结” 方案。核心思想是动静分离:让导致销毁的配置“静”下来,让变化的数据走“暗道”(Context)。

第一步:创建上下文与静态组件 (定义在组件外)

建立一个 Context 专门传输变化的 provided,并创建一个从 Context 拿数据的 Wrapper。

// 1. 定义 Context
const DragContext = React.createContext<DroppableProvided | undefined>(undefined);

// 2. 定义静态 Wrapper (永远不依赖 props 传 provided)
const DragBodyWrapper = (props: any) => {
  const provided = React.useContext(DragContext); // 👈 只有内部消费变化,不影响外层 DOM
  if (!provided) return <tbody {...props} />;
  return (
    <tbody {...props} {...provided.droppableProps} ref={provided.innerRef}>
      {props.children}
      {provided.placeholder}
    </tbody>
  );
};

// 3. 定义 Row 组件
const DraggableBodyRow = (props: any) => { ... };

第二步:冻结 Table 配置 (在组件内)

在主组件中,将 components 对象永久锁死,不依赖任何变量。

// 依赖为空数组,保证 components 引用在组件生命周期内永远不变
const components = useMemo(
  () => ({
    body: {
      wrapper: DragBodyWrapper,
      row: DraggableBodyRow,
    },
  }),
  []
);

第三步:稳定 Columns 配置

切断 columns 对动态数据的依赖。

// 1. 使用函数式更新 (prev => ...) 移除对 data 的依赖
const handleUpdate = useCallback((index, val) => {
  setData(prev => { ... });
}, []);

// 2. 缓存 columns
const columns = useMemo(() => [ ... ], [handleUpdate]);

第四步:注入 Context

在渲染 Table 时,用 Provider 包裹,将变化的 provided 传进去。

<Droppable droppableId="list">
  {(provided) => (
    // 👇 变化的 provided 走 Context 通道
    <DragContext.Provider value={provided}>
      <Table
        components={components} // 👈 这个 props 永远不变,Table 认为结构稳定
        columns={columns}       // 👈 这个 props 也很稳定
        dataSource={data}
        onRow={...}
      />
    </DragContext.Provider>
  )}
</Droppable>

4. 核心原理总结

  1. 欺骗 Table:通过 useMemo([], []) 让 Ant Design Table 认为 components 配置从未改变,从而复用 tbody DOM 节点,保留了 Input 的生存环境。
  2. 暗渡陈仓:利用 Contextreact-beautiful-dnd 必须的 refprops 直接注入到最底层的 tbody 中。React 只会进行最小粒度的属性 Diff 更新,而不会卸载组件。
  3. 双重保险:同时优化 columns,防止因列定义变化导致的单元格重绘。

5. 示例代码

点击查看完整示例代码