如何使用@dnd-kit实现多可排序容器?
关于@dnd-kit多可排序容器的实现疑问
我正在几个React项目中学习使用@dnd-kit,这个工具包功能强大、灵活且稳定,但文档有时不够清晰。另外他们有很多不错的示例,但示例没有附带代码说明具体实现方式。
我特别想复现多可排序容器的示例,想请教它的实际工作原理。
我已经尝试了一段时间,做出了一个接近的版本,但仍存在一些问题。以下是我目前的代码:
App.js代码:
import './App.css'; import { useState } from 'react' import {closestCenter, DndContext} from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy,} from '@dnd-kit/sortable'; import Lane from './Components/Lane'; function App() { const [todoItems, setTodoItems] = useState([ "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" ]); const [doneItems, setDoneItems] = useState([ "Item 6", "Item 7", "Item 8", "Item 9", "Item 10" ]); return ( <DndContext onDragOver={handleDragOver} onDragEnd={handleDragEnd} collisionDetection={closestCenter}> <div className="App" style={{display: 'flex', justifyContent: 'center', width: '100vw', height: '100vh', backgroundColor:'slateblue', padding: '2rem', boxSizing: 'border-box'}}> <SortableContext items={todoItems} strategy={verticalListSortingStrategy}> <Lane title="ToDo" items={todoItems} /> </SortableContext> <SortableContext items={doneItems} strategy={verticalListSortingStrategy}> <Lane title="Done" items={doneItems} /> </SortableContext> </div> </DndContext> ); function handleDragOver (e) { const {active, over} = e; if(over.id === 'Done' && !doneItems.includes(active.id)) { setDoneItems([...doneItems, active.id]) setTodoItems(todoItems.filter(item => item !== active.id)) } if(over.id === 'ToDo' && !todoItems.includes(active.id)) { setTodoItems([...todoItems, active.id]) setDoneItems(doneItems.filter(item => item !== active.id)) } } function handleDragEnd (e) { const {active, over} = e; const container = over.id; const title = active.data.current?.title ?? ""; const index = active.data.current?.index ?? 0; const parent = active.data.current?.parent ?? "ToDo"; } } export default App;
Lane.js代码:
import {useDroppable} from '@dnd-kit/core'; import Item from './Item'; const Lane = ({ title, items }) => { const {isOver, setNodeRef} = useDroppable({ id: title, }); const style = { backgroundColor: 'slategray', opacity: isOver ? '0.8' : '1', width: '300px', height: '500px', margin: '2rem' }; return ( <div ref={setNodeRef} style={style}> <h3>{title}</h3> {items.map((item, index) => { return <Item key={index} title={item} index={index} parent={title} /> })} </div> ) } export default Lane
Item.js代码:
import React from 'react'; import {useSortable} from '@dnd-kit/sortable'; import { CSS } from "@dnd-kit/utilities"; const Item = ({title, index, parent}) => { const {attributes, listeners, setNodeRef, transform} = useSortable({ id: title, data: { title, index, parent, } }); const style = { transform: transform ? CSS.Translate.toString(transform) : undefined, borderTop: '1px solid blue', padding: '1rem', boxShadow: '0px 0px 5px 2px #2121213b' } return ( <div ref={setNodeRef} style={style} {...listeners} {...attributes}> {title} </div> ) } export default Item
文档中提到:
在这个示例中,我们会使用
DndContext的onDragOver回调来检测可拖拽元素何时移动到不同容器上,以便在拖拽过程中将其插入到新容器中。
所以我在dragOver函数中做了类似处理,但我的dragEnd函数还没完成——我先尝试让第一部分正常工作。
求帮助!
问题分析与解决方案
核心问题点
onDragOver频繁触发状态更新:该回调会在拖拽过程中持续触发,当前逻辑会导致状态被重复修改,引发不必要的重渲染,甚至出现元素异常。- 容器内排序逻辑缺失:仅实现了跨容器转移,无法在同一容器内拖拽调整元素顺序。
useDroppable与SortableContext冲突:Lane组件的可拖拽目标会干扰Item的碰撞检测,导致拖拽时目标判断混乱。
正确实现思路
多可排序容器的核心是:
- 用全局
SortableContext管理所有可排序项,统一识别元素位置关系 - 仅在
onDragEnd中处理状态更新(仅触发一次),区分容器内排序和跨容器转移两种场景 - 移除冗余的
useDroppable,依赖SortableContext完成碰撞检测
修正后的代码
App.js
import './App.css'; import { useState, useCallback } from 'react' import { closestCenter, DndContext } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; import Lane from './Components/Lane'; function App() { const [todoItems, setTodoItems] = useState([ "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" ]); const [doneItems, setDoneItems] = useState([ "Item 6", "Item 7", "Item 8", "Item 9", "Item 10" ]); // 聚合所有可排序项ID,供全局SortableContext使用 const getAllItemIds = useCallback(() => [ ...todoItems, ...doneItems ], [todoItems, doneItems]); const handleDragEnd = useCallback((event) => { const { active, over } = event; if (!over || active.id === over.id) return; // 读取源容器和目标容器信息 const sourceContainer = active.data.current?.container; const targetContainer = over.data.current?.container; // 同一容器内排序 if (sourceContainer === targetContainer) { const sourceList = sourceContainer === 'ToDo' ? todoItems : doneItems; const setList = sourceContainer === 'ToDo' ? setTodoItems : setDoneItems; const oldIndex = sourceList.indexOf(active.id); const newIndex = sourceList.indexOf(over.id); if (oldIndex !== -1 && newIndex !== -1) { setList(arrayMove(sourceList, oldIndex, newIndex)); } } // 跨容器转移 else { // 从源容器移除元素 const sourceList = sourceContainer === 'ToDo' ? todoItems : doneItems; const setSourceList = sourceContainer === 'ToDo' ? setTodoItems : setDoneItems; setSourceList(sourceList.filter(item => item !== active.id)); // 插入到目标容器的对应位置 const targetList = targetContainer === 'ToDo' ? todoItems : doneItems; const setTargetList = targetContainer === 'ToDo' ? setTodoItems : setDoneItems; const insertIndex = targetList.indexOf(over.id); if (insertIndex !== -1) { const newTargetList = [...targetList]; newTargetList.splice(insertIndex, 0, active.id); setTargetList(newTargetList); } else { // 拖拽到容器空白区域时,添加到末尾 setTargetList([...targetList, active.id]); } } }, [todoItems, doneItems]); return ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <div className="App" style={{ display: 'flex', justifyContent: 'center', width: '100vw', height: '100vh', backgroundColor:'slateblue', padding: '2rem', boxSizing: 'border-box' }}> <SortableContext items={getAllItemIds()} strategy={verticalListSortingStrategy} > <Lane title="ToDo" items={todoItems} /> <Lane title="Done" items={doneItems} /> </SortableContext> </div> </DndContext> ); } export default App;
Lane.js
import Item from './Item'; const Lane = ({ title, items }) => { const style = { backgroundColor: 'slategray', width: '300px', height: '500px', margin: '2rem', padding: '0.5rem', boxSizing: 'border-box' }; return ( <div style={style}> <h3>{title}</h3> {items.map((item) => ( <Item key={item} id={item} container={title} /> ))} </div> ) } export default Lane
Item.js
import React from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from "@dnd-kit/utilities"; const Item = ({ id, container }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id, data: { container } }); const style = { transform: transform ? CSS.Translate.toString(transform) : undefined, transition, margin: '0.5rem 0', padding: '1rem', backgroundColor: 'white', boxShadow: '0px 0px 5px 2px #2121213b', cursor: 'grab' }; return ( <div ref={setNodeRef} style={style} {...listeners} {...attributes}> {id} </div> ) } export default Item
关键改动说明
- 全局
SortableContext:统一管理所有可排序元素,让@dnd-kit能识别完整的元素位置关系,支持容器内排序和跨容器拖拽。 onDragEnd统一处理逻辑:把所有状态更新放在拖拽结束时执行,避免频繁触发状态修改的问题,同时清晰区分两种拖拽场景。- 移除
useDroppable:依赖SortableContext的碰撞检测能力,拖拽到容器空白区域时自动将元素添加到容器末尾。 - 使用
arrayMove工具函数:利用@dnd-kit提供的工具简化数组内元素的位置交换逻辑,减少代码冗余。
内容的提问来源于stack exchange,提问作者DJFancyAl
相关产品推荐
相关产品推荐

