1. 问题现象
在使用 React + Ant Design Table + react-beautiful-dnd 实现可拖拽表格时,表格中包含可编辑的 Input 输入框。
- 现象:鼠标点击输入框,输入一个字符后,输入框立即失去焦点(Blur),必须重新点击才能继续输入。
- 后果:无法连续输入,用户体验极差。
2. 问题原因
这是 React 的渲染机制、引用相等性检查(Referential Equality)和组件生命周期共同作用的结果。
主要原因:
- 输入触发更新:用户输入字符 ->
setState更新数据 -> 组件重新渲染 (Re-render)。 - 生成新对象:
react-beautiful-dnd的<Droppable>回调函数执行,生成一个新的provided对象(引用变了)。 - 配置发生变化:新的
provided对象被传递给了 Table 的components属性(或者components本身就是每次渲染内联生成的)。 - Table 强制重置:Ant Design Table 检测到
components属性的引用发生了变化。为了保证渲染正确性,它销毁了旧的tbodyDOM 节点,并重新创建新的tbody。 - 焦点丢失:旧的
<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. 核心原理总结
- 欺骗 Table:通过
useMemo([], [])让 Ant Design Table 认为components配置从未改变,从而复用tbodyDOM 节点,保留了 Input 的生存环境。 - 暗渡陈仓:利用
Context将react-beautiful-dnd必须的ref和props直接注入到最底层的tbody中。React 只会进行最小粒度的属性 Diff 更新,而不会卸载组件。 - 双重保险:同时优化
columns,防止因列定义变化导致的单元格重绘。