如何使用 React Hooks 处理 React Svg 拖放

Posted

技术标签:

【中文标题】如何使用 React Hooks 处理 React Svg 拖放【英文标题】:How to handle React Svg Drag and Drop with React Hooks 【发布时间】:2019-04-26 17:41:18 【问题描述】:

我正在尝试在 SVG 形状上实现拖放。我成功地使它与使用类组件一起工作。这是代码沙箱的链接:https://codesandbox.io/s/qv81pq1roq

但是现在我想通过使用带有自定义 Hook 的新 React API 来提取这个逻辑,该 Hook 能够将此功能添加到功能组件中。我尝试了很多东西,但没有任何效果。这是我最后一次尝试:

https://codesandbox.io/s/2x2850vjk0

我怀疑我添加和删除事件监听器的方式有些问题......所以这是我的问题:

你认为将这个 DnD SVG 逻辑放到自定义 Hook 中是否可行?如果是,你知道我做错了什么吗?

【问题讨论】:

【参考方案1】:

我在这里修复了示例 - https://codesandbox.io/s/2w0oy6qnvn

你的钩子示例中有很多问题:

    setPositionsetState 不同。它不会进行浅合并,而是将整个对象替换为新值,因此您必须使用 Object.assign() 或扩展运算符与先前的值合并。此外,setPosition() 钩子接受一个回调值,该值提供先前的状态值作为第一个参数,如果您需要在设置新值时引用它。

    与类不同,handleMouseMove 函数在每次渲染中都会重新创建,因此在调用 document.addEventListener('mousemove', handleMouseMove) 时,document.removeEventListener('mousemove', handleMouseMove) 不再引用初始 handleMouseMove 值。解决方法是使用 useRef 创建一个在组件的整个生命周期中持续存在的对象,非常适合保留对函数的引用。

    handleMouseDown 中的事件参数和您在setPosition 中引用的事件参数不一样。因为 React 使用事件池并重用事件,setPosition 中的事件可能已经不同于传递给 handleMouseDown 的事件。解决这个问题的方法是先获取pageXpageY 的值,这样在setPosition 内就不需要依赖事件对象了。

我在下面的代码中注释了你需要注意的部分。

const Circle = () => 
  const [position, setPosition] = React.useState(
    x: 50,
    y: 50,
    coords: ,
  );
  
  // Use useRef to create the function once and hold a reference to it.
  const handleMouseMove = React.useRef(e => 
    setPosition(position => 
      const xDiff = position.coords.x - e.pageX;
      const yDiff = position.coords.y - e.pageY;
      return 
        x: position.x - xDiff,
        y: position.y - yDiff,
        coords: 
          x: e.pageX,
          y: e.pageY,
        ,
      ;
    );
  );

  const handleMouseDown = e => 
    // Save the values of pageX and pageY and use it within setPosition.
    const pageX = e.pageX; 
    const pageY = e.pageY;
    setPosition(position => Object.assign(, position, 
      coords: 
        x: pageX,
        y: pageY,
      ,
    ));
    document.addEventListener('mousemove', handleMouseMove.current);
  ;

  const handleMouseUp = () => 
    document.removeEventListener('mousemove', handleMouseMove.current);
    // Use Object.assign to do a shallow merge so as not to 
    // totally overwrite the other values in state.
    setPosition(position =>
      Object.assign(, position, 
        coords: ,
      )
    );
  ;

  return (
    <circle
      cx=position.x
      cy=position.y
      r=25
      fill="black"
      stroke="black"
      strokeWidth="1"
      onMouseDown=handleMouseDown
      onMouseUp=handleMouseUp
    />
  );
;

const App = () => 
  return (
    <svg
      style=
        border: '1px solid green',
        height: '200px',
        width: '100%',
      
    >
      <Circle />
    </svg>
  );
;

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

【讨论】:

非常感谢阳顺的完整回复!我不知道这种使用 React.useRef() 的方式也可以应用于函数,而不仅仅是 dom 节点……非常有趣。出于好奇,是否可以仅通过使用 javascript 来获取对函数的引用?最后,我能够将所有逻辑放到自定义 Hook 中。这是新的沙盒:codesandbox.io/s/50n0qoqrzn。对了,新的 Hook api 真的亮了! 是的,您可以使用纯 JS 来获取函数引用,这就是您在类组件版本中所做的。在函数中,我不认为你可以用你拥有的东西来做,因为函数没有状态/实例,你需要将函数的引用存储在函数组件之外的某个地方。很高兴您喜欢新的钩子 API!我也喜欢(: 我怎样才能对缩放行为做同样的事情? 很好的答案阳顺!为了安全起见,我会添加一个额外的钩子: useEffect(() => () => document.removeEventListener('mousemove', handleMouseMove.current));基本上,您要确保从文档中删除事件侦听器,以防组件在拖动时被删除。【参考方案2】:

这是一个通用的自定义钩子,我一直在使用它来注册 SVG 元素上的拖动事件。

import  useState, useEffect, useCallback, useRef  from 'react'

// You may need to edit this to serve your specific use case
function getPos(e)     
  return 
    x: e.pageX,
    y: e.pageY,
  


// from https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state    
function usePrevious(value) 
  const ref = useRef()
  useEffect(() => 
    ref.current = value
  )
  return ref.current


export function useDrag( onDrag, onDragStart, onDragEnd ) 
  const [isDragging, setIsDragging] = useState(false)

  const handleMouseMove = useCallback(
    (e) => 
      onDrag(getPos(e))
    , 
    [onDrag]
  )

  const handleMouseUp = useCallback(
    (e) => 
      onDragEnd(getPos(e))
      document.removeEventListener('mousemove', handleMouseMove);
      setIsDragging(false)
    , 
    [onDragEnd, handleMouseMove]
  )

  const handleMouseDown = useCallback(
    (e) => 
      onDragStart(getPos(e))
      setIsDragging(true)
      document.addEventListener('mousemove', handleMouseMove)
    , 
    [onDragStart, handleMouseMove]
  )

  const prevMouseMove = usePrevious(handleMouseMove)

  useEffect(
    () => 
      document.removeEventListener('mousemove', prevMouseMove);
      if(isDragging) 
        document.addEventListener('mousemove', handleMouseMove)
      
    ,
    [prevMouseMove, handleMouseMove, isDragging]
  )

  useEffect(
    () => 
      if (isDragging) 
        document.addEventListener('mouseup', handleMouseUp)
      
      return () => document.removeEventListener('mouseup', handleMouseUp)
    ,
    [isDragging, handleMouseUp]
  )

  return handleMouseDown

【讨论】:

Nathan,你为什么使用 usePrevious() 钩子,为什么只用于 mouseMove 而不是 mouseUp?顺便说一句,看起来 addEventLister() 和 removeEventlListener() 被多次调用。我在这里转载:codepen.io/markvital/pen/NWGaeRw

以上是关于如何使用 React Hooks 处理 React Svg 拖放的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 React Hooks 单独切换手风琴面板?

处理 React Hooks 中的选择选项

React:使用 Hooks 为深度嵌套对象设置状态

如何使用 React-Hooks 和 Apollo Client 在 React-GraphQL 中实现搜索功能?

等待带有 React Hooks 的 Redux Action

如何将 React Context 与 Hooks 一起使用?