react-dnd 的使用问题:简易实践、关键名词

Posted by CodingWithAlice on April 20, 2025

react-dnd 的使用问题:简易实践、关键名词

总结:

  • 建立排查逻辑流:从外向内,先 UI 观察差异/关注任何异常,不要忽略 chrome 工具的价值 - 热区问题的排查 - 直接修改 节点/属性值来定位 - 更快速高效
  • React-dnd 的简易实践:useDrag 记录源数据 id,主要是通过配置 item/type/collect/end -> useDragLayer 进行位置计算 -> useDrop 配置 accept/drop
  • 关键名词记忆
    • 项目:被拖动的内容
    • 监视器:拖放操作的状态 + 当前项目信息,可以定义一个收集函数 collect - dnd 会及时调用收集函数
    • 连接器:作为收集函数的第一个参数,预定义角色

一、React-dnd 的基本概念论述

1、功能

构建复杂的拖放问题,同时保持组件的解耦

  • 拖动会在应用的不同部分之间 传输数据,而组件根据 拖放事件 改变其外观、状态
2、依赖安装 + 使用
npm install react-dnd react-dnd-html5-backend
--
import { useDrag } from 'react-dnd';
export default function MyCon({ isDragging, text }) {
    const [{opacity}, dragRef] = useDrag(() => ({
        type: ItemTypes.CARD, // 场景少的话,可以直接写字符串,例如 ‘DragRow’
        item: { text },
        collect: (monitor) => ({
            opacity: monitor.isDragging() ? 0.5 : 1 
        })
    }), [])
    return (<div ref={dragRef} style=> {text} </div>)
}
3、理解

不封装组件,而是将道具注入你的组件

  • 内部使用了 redux 实现,架构相似
  • 名词 - 项目:这个名词用于描述 被拖动的内容 - 这么描述有助于保持组件间的分离和互不关联

通过 监视器 和 连接器 可以描述 React DnD 应该向组件注入哪些道具:

  • 名词 - 监视器:这个名词是存储了 拖放操作的状态 - 是否正在进行拖拽、当前项目信息
    • 每个需要跟踪拖放状态的组件,都可以定义一个 收集函数 ,用于在监视器中检索相关内容 - DnD 会及时调用收集函数
  • 名词 - 连接器:(后端处理 DOM 事件)用于为 render 函数中的 DOM 节点指定一个预定义角色(拖动源、拖动预览、下拉目标)
    • 作为收集函数的第一个参数
function collect(connect, monitor) {
    return {
        highlighted: monitor.canDrop(),
        hovered: monitor.isOver(),
        connectDropTarget: connect.dropTarget(),
    }
}

【主要抽象单元】 拖动源和拖放目标 - 将类型、项目、副作用和收集功能与您的组件联系在一起

  • 解决的问题: 如何配置 注入这些道具呢?如何执行副作用来响应拖放事件?

二、HOOKS

主要三个 hooks: useDrag、useDrop、useDragLayer

  • useDrop

作用: 将您的组件作为 投放目标 接入 DnD 系统, 指定下拉目标将接受哪些类型的数据项、收集哪些道具

返回值: 返回一个数组,其中包含附加到下拉目标节点的 ref 和收集的道具

三、完整工作流

step1 拖拽开始: useDrag 记录源项 index 和 id

step2 位置计算 + 状态更新:拖拽移动 handlePosition 通过鼠标位置计算 targetRow + 实时更新标记线位置

step3 放置确认:放置 drop 回调获取 源项 item.index 和 目标项 markLineState.targetRow

  • 再调用 onRowDragEnd(sourceIndex, targetRow) 修改数据

简易最佳实践

// step1:配置 useDrag
const DraggableItem = ({ index, rowHeight, handlePosition, dragId}) => {
    const [{ isDragging }, drag, preview] = useDrag(
        {
            type: 'DragRow',
            item: { id: dragId, index},
            collect: (monitor) => ({ isDragging: monitor.isDragging() }),
            end: (item, monitor) => {
                if (!monitor.didDrop()) { handlePosition(dragId) }
            },
        });

    const { items, currentOffset } = useDragLayer((monitor) => {
        let items = monitor.getItem();
        if (monitor.isDragging()) {
            handlePosition(dragId, currentOffset, items);
        }
        return {items, currentOffset: monitor.getSourceClientOffset()}
    });

    return (
        <div ref={preview} key={dragId} >
			<NavCell key={dragId} dragRef={drag} rowDrag={true} rowHeight={rowHeight} />
    	</div>
	);
};
// step2:计算位置 - 应该放在下方的 dragCol 中,但是逻辑其实和拖拽无关,是业务逻辑 - 所以抽出来展示理解
const handlePosition = useCallback((dragId, params, items) => {
    if (!params) {  
        setMarkLineState({ top: null })
        return;
    }
    if(items.id !== dragId) { // react-dnd 中monitor 唯一,需要通过组件 id 来判定是否重新计算 markLineState 的值
        return;
    }
    let clientInfo = refs.bodyRef.current.getOffset();
    let info = canvasCtx?.selectCellAreaMouseDown({clientX: params.x, clientY:params.y }, clientInfo);

    const { lastRow, lastTop } = dataRef.current;
    if (lastRow !== info?.row) { // 简单计算高度
        let row = info?.row === 0 ? 1 : info.row;
        const top = rowHeight * row;
        setMarkLineState({
            lastRow: row,
            lastTop: markLineState?.top,
            top
        })
    }
})
// step3:放置位置,触发 drop 实现拖拽
export default function index(props) {
    const [, drop] = useDrop({
        accept: 'DragRow',
        drop: (item, monitor) => {
            onDrop(item?.index) // 即 useDrag 中传递的 item 信息
        }
    }
                            );
    const onDrop = (sourceIndex) => {
        let targetRow = markLineState.lastRow;
        let sourceRow = sourceIndex; // 目标行 - 取 onDrop 监听到被拖拽的行
        if (sourceRow > targetRow) { onRowDragEnd(sourceIndex, targetRow) } // 上移
        if (sourceRow < targetRow) { onRowDragEnd(sourceIndex, targetRow - 1) } // 下移

        setMarkLineState({ lastRow: 0, lastTop: null, top: null })
    }
    return ( <div className={styles['drop-wrapper']} ref={drop}>
        {data.map((v, i) => (
            <DraggableItem 
            	key={v.id} dragId={v.id} index={i} rowHeight={rowHeight} handlePosition={handlePosition}/> ))
		}
    </div>)
}