这份代码是一个完整的、可运行的 TypeScript (TSX) 示例。它完美融合了 React、Ant Design Table 和 react-beautiful-dnd,并应用了我们之前讨论的“Context 穿透 + 引用冻结”方案,彻底解决了输入框焦点丢失的问题。
核心特点:
- 行拖拽:可以拖拽表格行进行排序。
- 行内编辑:包含 Input 输入框,输入时焦点不丢失。
- 代码规范:使用了 TypeScript 类型定义,且注释详细解释了核心优化点。
前置准备
你需要安装以下依赖:
npm install antd react-beautiful-dnd @types/react-beautiful-dnd
# 或者
yarn add antd react-beautiful-dnd @types/react-beautiful-dnd
完整示例代码
import React, { useState, useCallback, useMemo, useContext } from "react";
import { Table, Input, message } from "antd";
import type { ColumnsType } from "antd/es/table";
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
DroppableProvided,
DraggableProvided,
} from "react-beautiful-dnd";
import { MenuOutlined } from "@ant-design/icons";
// 定义数据类型
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
// ----------------------------------------------------------------------------
// 核心优化 1: 创建 Context
// 作用:用于跨层级传递 DroppableProvided,绕过 Table 的 props 更新机制
// ----------------------------------------------------------------------------
const DragContext = React.createContext<DroppableProvided | undefined>(
undefined
);
// ----------------------------------------------------------------------------
// 核心优化 2: 静态的 BodyWrapper 组件
// 作用:它从 Context 消费数据,而不是通过 props。
// 这样即使 provided 变了,Table 看到的这个组件引用也没变,就不会销毁 tbody。
// ----------------------------------------------------------------------------
const DragBodyWrapper = (
props: React.HTMLAttributes<HTMLTableSectionElement>
) => {
const provided = useContext(DragContext);
if (!provided) return <tbody {...props} />;
return (
<tbody {...props} {...provided.droppableProps} ref={provided.innerRef}>
{props.children}
{provided.placeholder}
</tbody>
);
};
// ----------------------------------------------------------------------------
// 静态的 Row 组件
// 作用:处理每一行的拖拽逻辑
// ----------------------------------------------------------------------------
const DraggableBodyRow = (props: any) => {
const { index, className, style, ...restProps } = props;
// 这里的 index 是通过 Table onRow 传入的
// data-row-key 是 antd 自动生成的
return (
<Draggable draggableId={restProps["data-row-key"]} index={index}>
{(provided: DraggableProvided, snapshot: any) => (
<tr
{...restProps}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...style,
...provided.draggableProps.style,
// 拖拽时给点背景色
background: snapshot.isDragging ? "#e6f7ff" : "inherit",
display: snapshot.isDragging ? "table" : "table-row",
}}
/>
)}
</Draggable>
);
};
const DraggableEditableTable: React.FC = () => {
// 初始化模拟数据
const [dataSource, setDataSource] = useState<DataType[]>([
{
key: "1",
name: "John Brown",
age: 32,
address: "New York No. 1 Lake Park",
},
{ key: "2", name: "Jim Green", age: 42, address: "London No. 1 Lake Park" },
{ key: "3", name: "Joe Black", age: 32, address: "Sidney No. 1 Lake Park" },
{ key: "4", name: "Mike Smith", age: 25, address: "Tokyo No. 1 Street" },
]);
// ----------------------------------------------------------------------------
// 核心优化 3: 稳定的数据更新函数
// 作用:使用函数式更新 (prev => ...),移除对 dataSource 的依赖。
// 配合 useCallback,保证 handleInputChange 这个函数引用永远不变。
// ----------------------------------------------------------------------------
const handleInputChange = useCallback((key: string, value: string) => {
setDataSource((prev) => {
const newData = [...prev];
const index = newData.findIndex((item) => item.key === key);
if (index > -1) {
newData[index] = { ...newData[index], name: value };
}
return newData;
});
}, []); // 依赖为空,函数句柄稳定
// 处理拖拽结束
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
const sourceIndex = result.source.index;
const destinationIndex = result.destination.index;
if (sourceIndex === destinationIndex) return;
const newData = Array.from(dataSource);
const [movedItem] = newData.splice(sourceIndex, 1);
newData.splice(destinationIndex, 0, movedItem);
setDataSource(newData);
message.success("排序已更新");
};
// ----------------------------------------------------------------------------
// 核心优化 4: 稳定的 Columns 定义
// 作用:依赖项包含 handleInputChange (它是稳定的),所以 columns 也是稳定的。
// 这防止了 Table 因为 columns 变化而重新渲染所有单元格。
// ----------------------------------------------------------------------------
const columns: ColumnsType<DataType> = useMemo(
() => [
{
title: "排序",
dataIndex: "sort",
width: 60,
align: "center",
render: () => (
<MenuOutlined style={{ cursor: "grab", color: "#999" }} />
),
},
{
title: "Name (Editable)",
dataIndex: "name",
render: (text, record) => (
<Input
value={text}
// 这里的 onChange 引用了上面稳定的 handleInputChange
onChange={(e) => handleInputChange(record.key, e.target.value)}
bordered={false} // 让它看起来像纯文本,点击变输入框
style={{ padding: 0 }}
/>
),
},
{
title: "Age",
dataIndex: "age",
},
{
title: "Address",
dataIndex: "address",
},
],
[handleInputChange]
); // 仅依赖 handleInputChange
// ----------------------------------------------------------------------------
// 核心优化 5: 冻结 components 配置
// 作用:依赖为空数组 [],保证 components 对象在组件生命周期内只有一份引用。
// Antd Table 发现 components 没变,就不会销毁 tbody DOM。
// ----------------------------------------------------------------------------
const components = useMemo(
() => ({
body: {
wrapper: DragBodyWrapper,
row: DraggableBodyRow,
},
}),
[]
);
return (
<div style={{ padding: 20 }}>
<h3>Ant Design Table + DND + Editable Input (Focus Fixed)</h3>
<p style={{ color: "#666", marginBottom: 20 }}>
试着在输入框连续输入字符,焦点不会丢失;同时支持按住左侧图标拖拽排序。
</p>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="draggable-table">
{(provided: DroppableProvided) => (
// ----------------------------------------------------------------------------
// 核心优化 6: Context 注入
// 作用:将变化的 provided 通过 Context 传给底层的 DragBodyWrapper。
// ----------------------------------------------------------------------------
<DragContext.Provider value={provided}>
<Table
columns={columns}
dataSource={dataSource}
rowKey="key"
components={components}
pagination={false}
// 将 index 传给 Row 组件
onRow={(record, index) =>
({
index,
"data-row-key": record.key,
} as any)
}
/>
</DragContext.Provider>
)}
</Droppable>
</DragDropContext>
</div>
);
};
export default DraggableEditableTable;
注意事项:React 18 StrictMode
如果你使用的是 React 18 并且在开发环境开启了 <React.StrictMode>,react-beautiful-dnd 可能会无法正常工作(这是该库的一个已知兼容性问题,与我们的 Context 方案无关)。