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

React TypeScript组件实现鼠标拖拽横向滚动问题求助

React横向拖拽滚动组件问题排查与修复

问题概述

我正在实现一个可复用React组件,支持鼠标点击拖拽实现横向滚动,效果类似触控板/触摸屏的滚动。

预期行为

被包裹的Div能跟随鼠标拖拽动作左右滚动

实际行为

点击拖拽无法触发滚动,但鼠标放在目标Div上横向移动时,Div会有轻微滚动效果。

我尝试过调整滚动条、增删事件监听器等方法,还试图把一个非React环境下的可用示例适配到React组件中。这个组件用来包裹应用中任何有横向溢出的div,通过useRef定位子div的逻辑是正常的。

原组件代码

import React, { useState, ReactNode, useRef, RefObject, useEffect } from "react";

type DraggableProps = {
  innerRef: RefObject<HTMLDivElement>;
  rootClass: string;
  children: ReactNode;
};

export const Draggable = ({ innerRef, rootClass = "", children }: DraggableProps) => {
  const slider = innerRef.current as HTMLElement;
  const ourRef = useRef<HTMLDivElement>(null);
  const [pageX, setPageX] = useState(0);
  const [scrollX, setScrollX] = useState(0);
  const [isScrolling, setIsScrolling] = useState(false);

  useEffect(() => {
    handleMouseDown()
  },[isScrolling])

  const handleMouseDown = () => {
      setIsScrolling(true);
    slider?.addEventListener("mousedown", (e) => {
      console.log("Mouse down => event", e);
      setPageX(e.pageX - slider.offsetLeft);
      setScrollX(slider?.scrollLeft);
    });
    slider?.addEventListener("mouseleave", () => {
      console.log("Mouse leave");
      slider.removeEventListener("mousedown", () => {console.log("remove mouse down");
      });
      slider.removeEventListener("mousemove", () => {console.log("remove mouse move");
      });
      setIsScrolling(false);
    });
    slider?.addEventListener("mouseup", () => {
      console.log("Mouse up");
      slider.removeEventListener("mousedown", () => {console.log("remove mouse down");
      });
      slider.removeEventListener("mousemove", () => {console.log("remove mouse move");
      });
      setIsScrolling(false);
    });
    slider?.addEventListener("mousemove", (e) => {
      if (!isScrolling) return;
      e.preventDefault();
      console.log("Mouse move => pageX", pageX);

      const x = e.pageX - slider?.offsetLeft;
      const walk = x - pageX;
      slider.scrollLeft = scrollX - walk;
    });  
  };

  return (
    <div ref={ourRef} onMouseDown={handleMouseDown} className={rootClass}>
      {children}
    </div>
  );
};

问题根源

  1. 事件监听器绑定逻辑错误:在handleMouseDown中重复绑定mousedown事件,且移除监听器时使用匿名函数,无法正确移除(每次创建的匿名函数都是新引用)。
  2. 状态更新时机问题:useEffect依赖isScrolling,每次状态变化都会调用handleMouseDown,导致重复绑定事件。
  3. 拖拽状态控制逻辑混乱:isScrolling在handleMouseDown一开始就被设为true,但实际应该在用户真正点击(mousedown触发)后才开启拖拽状态。
  4. React合成事件与原生事件混用冲突:组件根元素绑定了onMouseDown合成事件,同时又给slider绑定原生mousedown事件,导致逻辑冲突。

修复后的代码

import React, { useState, ReactNode, useRef, RefObject, useEffect } from "react";

type DraggableProps = {
  innerRef: RefObject<HTMLDivElement>;
  rootClass: string;
  children: ReactNode;
};

export const Draggable = ({ innerRef, rootClass = "", children }: DraggableProps) => {
  const [isDragging, setIsDragging] = useState(false);
  const startX = useRef(0);
  const startScrollLeft = useRef(0);

  useEffect(() => {
    const slider = innerRef.current;
    if (!slider) return;

    const handleMouseDown = (e: MouseEvent) => {
      setIsDragging(true);
      startX.current = e.pageX - slider.offsetLeft;
      startScrollLeft.current = slider.scrollLeft;
      // 防止文本选中干扰拖拽
      slider.style.userSelect = "none";
    };

    const handleMouseMove = (e: MouseEvent) => {
      if (!isDragging || !slider) return;
      e.preventDefault();
      const x = e.pageX - slider.offsetLeft;
      const walk = x - startX.current;
      slider.scrollLeft = startScrollLeft.current - walk;
    };

    const handleMouseUp = () => {
      setIsDragging(false);
      if (slider) {
        slider.style.userSelect = "";
      }
    };

    // 绑定事件
    slider.addEventListener("mousedown", handleMouseDown);
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);

    // 清理函数,组件卸载时移除事件
    return () => {
      slider.removeEventListener("mousedown", handleMouseDown);
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [innerRef, isDragging]);

  return (
    <div className={rootClass}>
      {children}
    </div>
  );
};

修复说明

  • 事件绑定优化:在useEffect中一次性绑定所有必要事件,使用具名函数确保能正确移除监听器,组件卸载时自动清理事件,避免内存泄漏。
  • 拖拽状态控制:只有在mousedown触发后才设置isDragging为true,确保拖拽逻辑仅在用户点击后生效。
  • 临时值存储优化:拖拽起始的startX和startScrollLeft使用useRef存储,避免因状态更新导致的重新渲染和值丢失问题。
  • 全局事件监听:将mousemove和mouseup绑定到document上,确保即使鼠标移出滑块区域,也能正常结束拖拽。
  • 用户体验优化:拖拽时禁用滑块内的文本选择,避免拖拽时选中文本影响操作体验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 13:38:00