You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 03:15:40