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

React自定义Resize Hook缩放异常求助:拖拽手柄无法跟随光标

元素缩放React Hook的精度问题分析

我开发了一个实现元素缩放的React Hook,参考相关回答完成代码编写。目前缩放功能可运行,但存在精度问题:缩放手柄无法跟随光标,移动轨迹杂乱无章。

效果预览:
缩放效果预览

Hook代码

interface Dimensions {
  width: number | null;
  height: number | null;
}

interface UseResizableProps {
  elementRef: React.RefObject<HTMLDivElement>;
  handleRef: React.RefObject<HTMLDivElement>;
}

function useResizable({ elementRef, handleRef }: UseResizableProps) {
  const dimensions = useRef<Dimensions>({ width: null, height: null });

  // 初始化获取元素尺寸
  useEffect(() => {
    if (elementRef.current) {
      const rect = elementRef.current.getBoundingClientRect();
      dimensions.current = { height: rect.height, width: rect.width };
    }
  }, [elementRef]);

  useEffect(() => {
    function handleMouseDownE(mouseDownEvent: MouseEvent) {
      const startPosition = {
        x: mouseDownEvent.pageX,
        y: mouseDownEvent.pageY,
      };
      function onMouseMove(mouseMoveEvent: MouseEvent) {
        if (
          dimensions.current.height === null ||
          dimensions.current.width === null
        ) {
          return;
        }
        dimensions.current = {
          width:
            dimensions.current.width - startPosition.x + mouseMoveEvent.pageX,
          height: dimensions.current.height,
        };

        if (
          elementRef?.current?.style.width !== undefined &&
          dimensions.current.width
        ) {
          const px = `${dimensions.current.width}px`;
          elementRef.current.style.width = px;
        }
      }
      function onMouseUp() {
        document.body.removeEventListener("mousemove", onMouseMove);
      }
      document.body?.addEventListener("mousemove", onMouseMove);
      document.body?.addEventListener("mouseup", onMouseUp, { once: true });
    }
    handleRef?.current?.addEventListener("mousedown", handleMouseDownE);
  });
}

Hook使用示例

function App() {
  const elementRef = useRef<HTMLDivElement>(null);
  const handleRef = useRef<HTMLDivElement>(null);

  useResizable({ elementRef, handleRef });

  return (
    <div className="App">
      <div className="resizable" ref={elementRef}>
        <div className="resize-handle" ref={handleRef}></div>
      </div>
    </div>
  );
}

样式代码

.resizable {
  width: 500px;
  height: 500px;
  background-color: lightpink;
  position: relative;
}

.resize-handle {
  width: 5px;
  height: 100%;
  background-color: orangered;
  right: 0;
  position: absolute;
  cursor: e-resize;
}

问题分析

  1. 宽度计算逻辑错误:
    每次鼠标移动时,代码用当前已更新的dimensions.current.width减去初始鼠标X再加上当前鼠标X,这会导致偏移量被重复累积。例如第一次移动后宽度变为初始宽度 + (当前X - 初始X),第二次移动时会基于这个新宽度再次计算,最终得到初始宽度 + 2*(当前X - 初始X),完全偏离了光标实际移动的距离。

  2. useEffect依赖缺失:
    第二个useEffect没有设置依赖数组,会导致组件每次渲染都重新绑定mousedown事件,可能出现重复绑定的情况,引发异常行为。

  3. 事件清理不完整:
    没有在useEffect的返回函数中移除mousedown事件绑定,当组件卸载时会造成内存泄漏。


修复后的Hook代码

interface Dimensions {
  width: number | null;
  height: number | null;
}

interface UseResizableProps {
  elementRef: React.RefObject<HTMLDivElement>;
  handleRef: React.RefObject<HTMLDivElement>;
}

function useResizable({ elementRef, handleRef }: UseResizableProps) {
  const dimensions = useRef<Dimensions>({ width: null, height: null });

  useEffect(() => {
    if (elementRef.current) {
      const rect = elementRef.current.getBoundingClientRect();
      dimensions.current = { height: rect.height, width: rect.width };
    }
  }, [elementRef]);

  useEffect(() => {
    const handleMouseDown = (mouseDownEvent: MouseEvent) => {
      // 保存鼠标按下时的初始状态:初始宽度和初始鼠标位置
      const initialWidth = dimensions.current.width;
      const startX = mouseDownEvent.pageX;

      const handleMouseMove = (mouseMoveEvent: MouseEvent) => {
        if (initialWidth === null) return;
        // 计算鼠标移动的差值,基于初始宽度更新
        const deltaX = mouseMoveEvent.pageX - startX;
        const newWidth = initialWidth + deltaX;
        
        // 可选:添加最小宽度限制,避免缩放到0或负数
        if (newWidth >= 100 && elementRef.current) {
          elementRef.current.style.width = `${newWidth}px`;
          // 更新ref中的尺寸
          dimensions.current.width = newWidth;
        }
      };

      const handleMouseUp = () => {
        document.body.removeEventListener("mousemove", handleMouseMove);
      };

      document.body.addEventListener("mousemove", handleMouseMove);
      document.body.addEventListener("mouseup", handleMouseUp, { once: true });
    };

    const currentHandle = handleRef.current;
    currentHandle?.addEventListener("mousedown", handleMouseDown);

    // 清理事件绑定
    return () => {
      currentHandle?.removeEventListener("mousedown", handleMouseDown);
    };
  }, [elementRef, handleRef]); // 添加依赖数组
}

内容的提问来源于stack exchange,提问作者devklick

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 05:28:12