如何在React循环中使用Ref数组替代单个Ref实现拖拽?
React Map循环中实现多元素拖拽的Ref数组解决方案
当前代码的问题在于useDraggable Hook仅维护单个元素的状态与ref,在map循环中每次赋值都会覆盖ref指向,最终只有最后一个元素能响应拖拽。以下是不引入新组件的修改方案:
1. 修改useDraggable Hook,支持多元素状态管理
将单个元素的状态(位置、解绑函数)改为按索引存储,同时让ref回调接收元素索引,绑定对应元素的事件:
import React, { useRef, useState, useEffect, useCallback } from "react"; export const throttle = (f) => { let token = null, lastArgs = null; const invoke = () => { f(...lastArgs); token = null; }; const result = (...args) => { lastArgs = args; if (!token) { token = requestAnimationFrame(invoke); } }; result.cancel = () => token && cancelAnimationFrame(token); return result; }; const id = (x) => x; const useDraggable = ({ onDrag = id } = {}) => { // 记录当前正在拖拽的元素索引,null表示未拖拽 const [activeIndex, setActiveIndex] = useState(null); // 存储每个元素的位置,key为索引 const positions = useRef({}); // 存储所有元素的ref const refs = useRef([]); // 存储每个元素的解绑函数 const unsubscribes = useRef([]); const legacyRef = useCallback((elem, index) => { refs.current[index] = elem; // 先解绑之前的事件 if (unsubscribes.current[index]) { unsubscribes.current[index](); } if (!elem) { return; } const handleMouseDown = (e) => { e.target.style.userSelect = "none"; // 初始化位置(如果之前没存过) if (!positions.current[index]) { positions.current[index] = { x: 0, y: 0 }; } setActiveIndex(index); }; elem.addEventListener("mousedown", handleMouseDown); unsubscribes.current[index] = () => { elem.removeEventListener("mousedown", handleMouseDown); }; }, []); useEffect(() => { if (activeIndex === null) { return; } const handleMouseMove = throttle((event) => { const elem = refs.current[activeIndex]; const pos = positions.current[activeIndex]; if (!elem || !pos) { return; } // 更新当前元素的位置 positions.current[activeIndex] = onDrag({ x: pos.x + event.movementX, y: pos.y + event.movementY }); elem.style.transform = `translate(${positions.current[activeIndex].x}px, ${positions.current[activeIndex].y}px)`; }); const handleMouseUp = (e) => { e.target.style.userSelect = "auto"; setActiveIndex(null); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { handleMouseMove.cancel(); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [activeIndex, onDrag]); // 返回ref回调和当前是否有元素在拖拽的状态 return [legacyRef, activeIndex !== null]; };
2. 修改组件调用,传递元素索引
在map循环中,给ref回调传入当前元素的索引,让Hook能区分不同元素:
const quickAndDirtyStyle = { width: "200px", height: "200px", background: "#FF9900", color: "#FFFFFF", display: "flex", justifyContent: "center", alignItems: "center", margin: "10px" // 增加margin避免元素重叠 }; const DraggableComponent = () => { const handleDrag = useCallback( ({ x, y }) => ({ x: Math.max(0, x), y: Math.max(0, y) }), [] ); const [ref, isDragging] = useDraggable({ onDrag: handleDrag }); return ( <> {[1, 2, 3].map((el, i) => ( <div key={"element" + i} ref={(elem) => ref(elem, i)} style={quickAndDirtyStyle}> <p>{isDragging ? "Dragging..." : "Press to drag"}</p> </div> ))} </> ); }; export default function App() { return ( <div className="App"> <DraggableComponent /> </div> ); }
修改说明
- 将单个元素的状态(位置、解绑函数)改为按索引存储,确保每个元素拥有独立的拖拽状态
- ref回调新增索引参数,实现对多个元素的ref绑定与区分
- 拖拽逻辑仅针对当前激活的元素索引进行操作,避免干扰其他元素
- 保留原有的节流函数与拖拽边界控制逻辑,无需额外引入新组件
内容的提问来源于stack exchange,提问作者zEn feeLo
相关产品推荐
相关产品推荐

