ReactJS 钩子 - 拖放多个 useState 钩子和样式组件

ReactJS hooks - drag and drop with multiple useState hooks and styled-components

我对挂钩相当陌生,我正在尝试实现一个拖放容器组件,该组件在整个鼠标移动过程中处理 onDragStart、onDrag 和 onDragEnd 函数。我一直在尝试使用钩子复制此处找到的代码:https://medium.com/@crazypixel/mastering-drag-drop-with-reactjs-part-01-39bed3d40a03

我几乎已经使用下面的代码让它工作了。它使用样式化的组件进行动画处理。问题是它只有在您缓慢移动鼠标时才有效。如果您快速移动鼠标,SVG 或此 div 中包含的任何内容都会被抛出屏幕。

我有一个 component.js 文件看起来像

import React, { useState, useEffect, useCallback } from 'react';
import { Container } from './style'

const Draggable = ({children, onDragStart, onDrag, onDragEnd, xPixels, yPixels, radius}) => {
  const [isDragging, setIsDragging] = useState(false);
  const [original, setOriginal] = useState({
    x: 0,
    y: 0
  });
  const [translate, setTranslate] = useState({
    x: xPixels,
    y: yPixels
  });
  const [lastTranslate, setLastTranslate] = useState({
    x: xPixels,
    y: yPixels
  });

  useEffect(() =>{
    setTranslate({
      x: xPixels,
      y: yPixels
    });
    setLastTranslate({
      x: xPixels,
      y: yPixels
    })
  }, [xPixels, yPixels]);

  const handleMouseMove = useCallback(({ clientX, clientY }) => {

    if (!isDragging) {
      return;
    }
    setTranslate({
      x: clientX - original.x + lastTranslate.x,
      y: clientY - original.y + lastTranslate.y
    });
  }, [isDragging, original,  lastTranslate, translate]);



  const handleMouseUp = useCallback(() => {
    window.removeEventListener('mousemove', handleMouseMove);
    window.removeEventListener('mouseup', handleMouseUp);

    setOriginal({
      x:0,
      y:0
    });
    setLastTranslate({
      x: translate.x,
      y: translate.y
    });

    setIsDragging(false);
    if (onDragEnd) {
      onDragEnd();
    }

  }, [isDragging, translate, lastTranslate]);

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp)
    };
  }, [handleMouseMove, handleMouseUp]);

  const handleMouseDown = ({ clientX, clientY }) =>{

    if (onDragStart) {
      onDragStart();
    }
    setOriginal({
      x: clientX,
      y: clientY
    });
    setIsDragging(true);
  };

  return(
    <Container
      onMouseDown={handleMouseDown}
      x={translate.x}
      y={translate.y}
      {...{radius}}
      isDragging={isDragging}
    >
      {children}
    </Container>
  )
};

export default Draggable

样式组件文件 styled.js 如下所示:

import styled from 'styled-components/macro';

const Container = styled.div.attrs({
  style: ({x,y, radius}) => ({
    transform: `translate(${x - radius}px, ${y - radius}px)`
  })
})`
  //cursor: grab;
  position: absolute;

  ${({isDragging}) =>
    isDragging && `

    opacity: 0.8
    cursor: grabbing
  `}
`;

export {
  Container
}

所以我一开始就传入了父级的初始值。我认为我没有正确处理 useEffect / useState 并且获取信息的速度不够快。

如果有人能帮我弄清楚如何解决这个问题,我将不胜感激。再次抱歉,我对使用钩子还很陌生。

谢谢你:)

理想情况下,由于 setState 是异步的,您可以将所有状态移动到一个 object(如中等示例所做的那样)。然后,您可以利用 setState callback 来确保每个 event listenerevent callback 使用的值在调用 setState 时都是 up-to-date。

我认为那篇媒体文章中的例子也有同样的跳跃问题(这可能是示例视频缓慢移动对象的原因),但没有一个有效的例子,很难说。也就是说,为了解决这个问题,我删除了 originalXoriginalYlastTranslateXlastTranslateY 值,因为我们利用 setState回调。

此外,我将 event listeners/callbacks 简化为:

  • mousedown => 鼠标左键单击按住设置 isDragging true
  • mousemove => 鼠标移动更新 translateXtranslateY 通过 clientXclientY 更新
  • mouseup => 鼠标左键单击释放设置 isDragging 为 false。

这确保只有一个事件侦听器实际转换 xy 值。

如果您想利用此示例包含多个圆圈,则需要重新使用下面的组件或使用 useRef 并利用 refs 移动所选的圆圈;但是,这超出了您最初问题的范围。

最后,我还解决了 styled-components 弃用问题,方法是将 styled.div.data.attr 重组为 function,returns 为 style 属性 与 CSS,而不是 objectstyle 属性 是 function returns CSS.

已弃用:

styled.div.attrs({
  style: ({ x, y, radius }) => ({
    transform: `translate(${x - radius}px, ${y - radius}px)`
  })
})`

已更新:

styled.div.attrs(({ x, y, radius }) => ({
  style: {
    transform: `translate(${x - radius}px, ${y - radius}px)`
  }
}))`

工作示例:


components/Circle

import styled from "styled-components";

const Circle = styled.div.attrs(({ x, y, radius }) => ({
  style: {
    transform: `translate(${x - radius}px, ${y - radius}px)`
  }
}))`
  cursor: grab;
  position: absolute;
  width: 25px;
  height: 25px;
  background-color: red;
  border-radius: 50%;

  ${({ isDragging }) =>
    isDragging &&
    `
    opacity: 0.8;
    cursor: grabbing;
  `}
`;

export default Circle;

components/Draggable

import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import Circle from "../Circle";

const Draggable = ({ position, radius }) => {
  const [state, setState] = useState({
    isDragging: false,
    translateX: position.x,
    translateY: position.y
  });

  // mouse move
  const handleMouseMove = useCallback(
    ({ clientX, clientY }) => {
      if (state.isDragging) {
        setState(prevState => ({
          ...prevState,
          translateX: clientX,
          translateY: clientY
        }));
      }
    },
    [state.isDragging]
  );

  // mouse left click release
  const handleMouseUp = useCallback(() => {
    if (state.isDragging) {
      setState(prevState => ({
        ...prevState,
        isDragging: false
      }));
    }
  }, [state.isDragging]);

  // mouse left click hold
  const handleMouseDown = useCallback(() => {
    setState(prevState => ({
      ...prevState,
      isDragging: true
    }));
  }, []);

  // adding/cleaning up mouse event listeners
  useEffect(() => {
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseup", handleMouseUp);
    };
  }, [handleMouseMove, handleMouseUp]);

  return (
    <Circle
      isDragging={state.isDragging}
      onMouseDown={handleMouseDown}
      radius={radius}
      x={state.translateX}
      y={state.translateY}
    />
  );
};

// prop type schema
Draggable.propTypes = {
  position: PropTypes.shape({
    x: PropTypes.number,
    y: PropTypes.number
  }),
  radius: PropTypes.number
};

// default props if none are supplied
Draggable.defaultProps = {
  position: {
    x: 20,
    y: 20
  },
  radius: 10,
};

export default Draggable;