Tanstack Table React行拖拽排序功能失效问题排查
TanStack Table v8 拖拽排序功能无法工作的问题排查与修复
我正在React.js中开发一款支持行选择、拖拽排序(drag&drop)、筛选、分页等功能的轻量级表格,用于处理数百条数据。目前聚焦拖拽排序功能,但基于Tanstack Table v8的实现无法正常工作。已参考官方可行示例复刻,但仍未解决问题,请问是否遗漏了某些配置项?
我的实现代码
// https://tanstack.com/table/v8/docs/framework/react/examples/row-selection import React from "react"; import { ColumnDef, getCoreRowModel, useReactTable, flexRender, } from "@tanstack/react-table"; import { DndContext, closestCenter } from "@dnd-kit/core"; import { arrayMove, SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import styles from "./Table.module.css"; // Drag handle for rows function DragHandle({ rowId }) { const { attributes, listeners } = useSortable({ id: rowId }); return ( <button {...attributes} {...listeners} className={styles.dragHandle} title="Drag to reorder" > 🟰 </button> ); } // Row Component function Row({ row }) { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: row.id, }); const style = { transform: CSS.Transform.toString(transform), transition, }; return ( <tr ref={setNodeRef} style={style} {...attributes} {...listeners} className={styles.row} > {row.getVisibleCells().map((cell, index) => ( <td key={cell.id} className={styles.cell}> {index === 0 ? <DragHandle rowId={row.id} /> : null} {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ); } // Table Component function Table({ data, setData, columns }) { const tableColumns = React.useMemo(() => [...columns], [columns]); const table = useReactTable({ data, columns: tableColumns, getCoreRowModel: getCoreRowModel(), }); const handleDragEnd = (event) => { const { active, over } = event; if (!active?.id || !over?.id || active.id === over.id) { return; } setData((oldData) => { const oldIndex = oldData.findIndex((row) => row.id === active.id); const newIndex = oldData.findIndex((row) => row.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { return arrayMove(oldData, oldIndex, newIndex); } return oldData; }); }; return ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <SortableContext items={data.map((row) => row.id)} strategy={verticalListSortingStrategy} > <table className={styles.table}> <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id} className={styles.headerRow}> {headerGroup.headers.map((header) => ( <th key={header.id} className={styles.headerCell}> {flexRender( header.column.columnDef.header, header.getContext() )} </th> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map((row) => ( <Row key={row.id} row={row} /> ))} </tbody> </table> </SortableContext> </DndContext> ); } export default Table;
官方示例代码
import React, { CSSProperties } from 'react' import ReactDOM from 'react-dom/client' import './index.css' import { ColumnDef, Row, flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table' import { makeData, Person } from './makeData' // needed for table body level scope DnD setup import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, type DragEndEvent, type UniqueIdentifier, useSensor, useSensors, } from '@dnd-kit/core' import { restrictToVerticalAxis } from '@dnd-kit/modifiers' import { arrayMove, SortableContext, verticalListSortingStrategy, } from '@dnd-kit/sortable' // needed for row & cell level scope DnD setup import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' // Cell Component const RowDragHandleCell = ({ rowId }: { rowId: string }) => { const { attributes, listeners } = useSortable({ id: rowId, }) return ( // Alternatively, you could just set up dnd events on the rows themselves <button {...attributes} {...listeners}> 🟰 </button> ) } // Row Component const DraggableRow = ({ row }: { row: Row<Person> }) => { const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original.userId, }) const style: CSSProperties = { transform: CSS.Transform.toString(transform), //let dnd-kit do its thing transition: transition, opacity: isDragging ? 0.8 : 1, zIndex: isDragging ? 1 : 0, position: 'relative', } return ( // connect row ref to dnd-kit, apply important styles <tr ref={setNodeRef} style={style}> {row.getVisibleCells().map(cell => ( <td key={cell.id} style={{ width: cell.column.getSize() }}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ) } // Table Component function App() { const columns = React.useMemo<ColumnDef<Person>[]>( () => [ // Create a dedicated drag handle column. Alternatively, you could just set up dnd events on the rows themselves. { id: 'drag-handle', header: 'Move', cell: ({ row }) => <RowDragHandleCell rowId={row.id} />, size: 60, }, { accessorKey: 'firstName', cell: info => info.getValue(), }, { accessorFn: row => row.lastName, id: 'lastName', cell: info => info.getValue(), header: () => <span>Last Name</span>, }, { accessorKey: 'age', header: () => 'Age', }, { accessorKey: 'visits', header: () => <span>Visits</span>, }, { accessorKey: 'status', header: 'Status', }, { accessorKey: 'progress', header: 'Profile Progress', }, ], [] ) const [data, setData] = React.useState(() => makeData(20)) const dataIds = React.useMemo<UniqueIdentifier[]>( () => data?.map(({ userId }) => userId), [data] ) const rerender = () => setData(() => makeData(20)) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getRowId: row => row.userId, //required because row indexes will change debugTable: true, debugHeaders: true, debugColumns: true, }) // reorder rows after drag & drop function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (active && over && active.id !== over.id) { setData(data => { const oldIndex = dataIds.indexOf(active.id) const newIndex = dataIds.indexOf(over.id) return arrayMove(data, oldIndex, newIndex) //this is just a splice util }) } } const sensors = useSensors( useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}) ) return ( // NOTE: This provider creates div elements, so don't nest inside of <table> elements <DndContext collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis]} onDragEnd={handleDragEnd} sensors={sensors} > <div className="p-2"> <div className="h-4" /> <div className="flex flex-wrap gap-2"> <button onClick={() => rerender()} className="border p-1"> Regenerate </button> </div> <div className="h-4" /> <table> <thead> {table.getHeaderGroups().map(headerGroup => ( <tr key={headerGroup.id}> {headerGroup.headers.map(header => ( <th key={header.id} colSpan={header.colSpan}> {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} </th> ))} </tr> ))} </thead> <tbody> <SortableContext items={dataIds} strategy={verticalListSortingStrategy} > {table.getRowModel().rows.map(row => ( <DraggableRow key={row.id} row={row} /> ))} </SortableContext> </tbody> </table> <pre>{JSON.stringify(data, null, 2)}</pre> </div> </DndContext> ) } const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( <React.StrictMode> <App /> </React.StrictMode> )
问题排查与修复方案
对比你的代码和官方示例,以下几个关键配置项是你遗漏的:
1. 缺少getRowId配置项
TanStack Table默认使用数组索引作为行ID,但拖拽排序会改变行的索引位置,导致表格无法正确识别行的唯一性。
- 修复方法:在
useReactTable配置中添加getRowId,指定数据中唯一标识字段:
const table = useReactTable({ data, columns: tableColumns, getCoreRowModel: getCoreRowModel(), getRowId: row => row.id, // 新增该行 });
2. SortableContext的位置错误
官方示例中SortableContext嵌套在<tbody>内部,而你的代码将它包裹了整个<table>元素。表格DOM结构有严格层级要求,SortableContext生成的div元素会破坏表格合法结构,导致拖拽行为异常。
- 修复方法:将
SortableContext移动到<tbody>标签内部:
<tbody> <SortableContext items={data.map((row) => row.id)} strategy={verticalListSortingStrategy} > {table.getRowModel().rows.map((row) => ( <Row key={row.id} row={row} /> ))} </SortableContext> </tbody>
3. 未配置拖拽传感器和轴限制
你的DndContext缺少sensors配置(识别鼠标、触摸、键盘拖拽事件)和modifiers配置(限制拖拽为垂直方向),会导致拖拽触发困难或行为不符合预期。
- 修复方法:添加相关导入并配置:
// 导入缺失的模块 import { KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; // 在Table组件内部创建sensors const sensors = useSensors( useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}) ); // 更新DndContext配置 <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd} sensors={sensors} // 新增 modifiers={[restrictToVerticalAxis]} // 新增 >
4. 拖拽行的样式缺失关键属性
官方示例的拖拽行添加了position: relative、zIndex和opacity等样式,确保拖拽时行的视觉层级和透明度正常,避免视觉错乱。
- 修复方法:更新Row组件的style配置:
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.id, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.8 : 1, zIndex: isDragging ? 1 : 0, position: 'relative', };
5. DragHandle与Row的事件冲突
你的Row组件同时绑定了useSortable的attributes和listeners,而DragHandle也绑定了相同事件,会导致事件冒泡和冲突,拖拽触发逻辑混乱。
- 修复方法:移除Row组件上的
attributes和listeners绑定,只保留DragHandle上的拖拽事件:
function Row({ row }) { const { setNodeRef, transform, transition, isDragging } = useSortable({ id: row.id, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.8 : 1, zIndex: isDragging ? 1 : 0, position: 'relative', }; return ( <tr ref={setNodeRef} style={style} className={styles.row} > {/* ... 其他代码不变 */} </tr> ); }
内容的提问来源于stack exchange,提问作者Martin
相关产品推荐
相关产品推荐

