React DnD:可放置容器内拖拽排序避免复制问题的实现方案
问题描述
我已实现将可拖拽组件拖入可放置容器的功能,但在尝试实现可放置容器内部的拖拽排序时,操作会生成副本。我需要实现容器内原有项的拖拽排序,而非生成副本,请问该如何实现?
原代码
import React, { useState } from "react"; import { useDrag, useDrop } from "react-dnd"; import "../Styles/DroppableComponent.css"; const DragDropItem = ({ item, onDrop, index }) => { const [{ isDragging }, drag] = useDrag({ type: "DRAGGABLE_ITEM_TYPE", item: { id: item.id, index }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), dragPreviewType: "none", }); const itemStyles = { backgroundColor: "#f0f0f0", border: isDragging ? "5px dashed #ccc" : "1px solid transparent", padding: "8px", }; return ( <div ref={drag} style={itemStyles} onClick={() => onDrop(item.id, index)}> <h3>{item.title}</h3> <ul> {item.names?.map((name, nameIndex) => ( <li key={nameIndex}>{name}</li> ))} </ul> </div> ); }; const DroppableComponent = ({ data }) => { const [droppedItemIds, setDroppedItemIds] = useState([]); const handleDrop = (itemId, index) => { const item = data.find((item) => item.id === itemId); if (item) { const sameCategoryItems = droppedItemIds.filter( (id) => data.find((dataItem) => dataItem.id === id)?.category === item.category ); const hoverIndex = index; const insertIndex = sameCategoryItems.findIndex((id) => data.find((dataItem) => dataItem.id === id)?.category === item.category ? hoverIndex < sameCategoryItems.indexOf(id) : false ); const newDroppedItemIds = [...droppedItemIds]; newDroppedItemIds.splice( insertIndex === -1 ? sameCategoryItems.length : insertIndex, 0, itemId ); setDroppedItemIds(newDroppedItemIds); } }; const [{ isOver, canDrop }, drop] = useDrop({ accept: "DRAGGABLE_ITEM_TYPE", drop: (item) => { handleDrop(item.id, droppedItemIds.length); }, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }); const gridStyles = { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", border: isOver && canDrop ? "2px dashed #ccc" : "2px dashed transparent", padding: "8px", }; return ( <div ref={drop} style={{ border: "2px dashed #ccc", padding: "16px" }}> <h2>Draggable and Droppable Container</h2> <div className="grid" style={gridStyles}> {droppedItemIds.map((itemId, index) => { const item = data.find((dataItem) => dataItem.id === itemId); return item ? ( <DragDropItem key={itemId} item={item} onDrop={handleDrop} index={index} /> ) : null; })} </div> </div> ); }; export default DroppableComponent;
解决方案
问题核心是handleDrop未区分外部拖入和内部排序场景:内部拖拽时直接插入新ID,但原位置ID未被移除,导致生成副本。修改思路如下:
- 拆分逻辑,分别处理内部排序和外部拖入
- 为内部拖拽传递原位置索引,用于移动时的位置计算
- 给容器内每个item添加接收拖拽的能力,实现精确位置排序
修改后的完整代码:
import React, { useState } from "react"; import { useDrag, useDrop } from "react-dnd"; import "../Styles/DroppableComponent.css"; const DragDropItem = ({ item, index }) => { const [{ isDragging }, drag] = useDrag({ type: "DRAGGABLE_ITEM_TYPE", item: { id: item.id, originalIndex: index }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), dragPreviewType: "none", }); const itemStyles = { backgroundColor: "#f0f0f0", opacity: isDragging ? 0.5 : 1, border: isDragging ? "5px dashed #ccc" : "1px solid transparent", padding: "8px", cursor: "move", }; return ( <div ref={drag} style={itemStyles}> <h3>{item.title}</h3> <ul> {item.names?.map((name, nameIndex) => ( <li key={nameIndex}>{name}</li> ))} </ul> </div> ); }; const DroppableComponent = ({ data }) => { const [droppedItemIds, setDroppedItemIds] = useState([]); // 处理容器内item的位置移动 const handleMoveItem = (originalIndex, targetIndex) => { const newIds = [...droppedItemIds]; const [movedItemId] = newIds.splice(originalIndex, 1); newIds.splice(targetIndex, 0, movedItemId); setDroppedItemIds(newIds); }; // 处理外部item拖入容器 const handleAddItem = (itemId) => { if (droppedItemIds.includes(itemId)) return; const item = data.find((dataItem) => dataItem.id === itemId); if (!item) return; const sameCategoryItems = droppedItemIds.filter( (id) => data.find((dataItem) => dataItem.id === id)?.category === item.category ); const insertIndex = sameCategoryItems.length; const newIds = [...droppedItemIds]; newIds.splice(insertIndex, 0, itemId); setDroppedItemIds(newIds); }; // 容器级别的drop处理(接收外部拖入) const [{ isOver, canDrop }, containerDrop] = useDrop({ accept: "DRAGGABLE_ITEM_TYPE", drop: (draggedItem) => { if (!droppedItemIds.includes(draggedItem.id)) { handleAddItem(draggedItem.id); } }, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }); // 每个item的drop处理(接收内部拖拽) const ItemDropWrapper = ({ index, children }) => { const [{ isOver: isItemOver }, itemDrop] = useDrop({ accept: "DRAGGABLE_ITEM_TYPE", drop: (draggedItem) => { if (droppedItemIds.includes(draggedItem.id)) { handleMoveItem(draggedItem.originalIndex, index); } }, collect: (monitor) => ({ isOver: monitor.isOver(), }), }); const wrapperStyles = { padding: "4px", backgroundColor: isItemOver ? "#e0e0e0" : "transparent", }; return <div ref={itemDrop} style={wrapperStyles}>{children}</div>; }; const gridStyles = { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", border: isOver && canDrop ? "2px dashed #ccc" : "2px dashed transparent", padding: "8px", }; return ( <div ref={containerDrop} style={{ border: "2px dashed #ccc", padding: "16px" }}> <h2>Draggable and Droppable Container</h2> <div className="grid" style={gridStyles}> {droppedItemIds.map((itemId, index) => { const item = data.find((dataItem) => dataItem.id === itemId); return item ? ( <ItemDropWrapper key={itemId} index={index}> <DragDropItem item={item} index={index} /> </ItemDropWrapper> ) : null; })} </div> </div> ); }; export default DroppableComponent;
关键修改说明
- 拆分逻辑:将原
handleDrop拆分为handleMoveItem(内部排序)和handleAddItem(外部拖入),避免逻辑混淆 - 传递原索引:在
useDrag的item中携带originalIndex,用于内部移动时定位原位置 - Item级Drop:新增
ItemDropWrapper组件,让每个item都能接收拖拽,实现精确位置排序 - 防重复添加:外部拖入时先检查item是否已存在,避免重复
- 视觉优化:拖拽时添加透明效果,hover目标位置时高亮背景,提升操作反馈
内容的提问来源于stack exchange,提问作者Josna Reddy
相关产品推荐
相关产品推荐

