如何使用React挂钩处理React Svg拖放

时间:2018-11-24 12:15:26

标签: javascript reactjs svg drag-and-drop react-hooks

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

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

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

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

您认为是否有可能将此DnD SVG逻辑放入自定义的Hook?如果是,您知道我在做什么错吗?

2 个答案:

答案 0 :(得分:2)

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

您的钩子示例中存在许多问题:

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

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

  3. 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>

答案 1 :(得分:0)

这是我用来在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
}