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 listener
和 event callback
使用的值在调用 setState
时都是 up-to-date。
我认为那篇媒体文章中的例子也有同样的跳跃问题(这可能是示例视频缓慢移动对象的原因),但没有一个有效的例子,很难说。也就是说,为了解决这个问题,我删除了 originalX
、originalY
、lastTranslateX
、lastTranslateY
值,因为我们利用 setState
回调。
此外,我将 event listeners
/callbacks
简化为:
mousedown
=> 鼠标左键单击按住设置 isDragging
true
mousemove
=> 鼠标移动更新 translateX
和 translateY
通过 clientX
和 clientY
更新
mouseup
=> 鼠标左键单击释放设置 isDragging
为 false。
这确保只有一个事件侦听器实际转换 x
和 y
值。
如果您想利用此示例包含多个圆圈,则需要重新使用下面的组件或使用 useRef
并利用 refs
移动所选的圆圈;但是,这超出了您最初问题的范围。
最后,我还解决了 styled-components
弃用问题,方法是将 styled.div.data.attr
重组为 function
,returns 为 style
属性 与 CSS
,而不是 object
与 style
属性 是 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;
我对挂钩相当陌生,我正在尝试实现一个拖放容器组件,该组件在整个鼠标移动过程中处理 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 listener
和 event callback
使用的值在调用 setState
时都是 up-to-date。
我认为那篇媒体文章中的例子也有同样的跳跃问题(这可能是示例视频缓慢移动对象的原因),但没有一个有效的例子,很难说。也就是说,为了解决这个问题,我删除了 originalX
、originalY
、lastTranslateX
、lastTranslateY
值,因为我们利用 setState
回调。
此外,我将 event listeners
/callbacks
简化为:
mousedown
=> 鼠标左键单击按住设置isDragging
truemousemove
=> 鼠标移动更新translateX
和translateY
通过clientX
和clientY
更新mouseup
=> 鼠标左键单击释放设置isDragging
为 false。
这确保只有一个事件侦听器实际转换 x
和 y
值。
如果您想利用此示例包含多个圆圈,则需要重新使用下面的组件或使用 useRef
并利用 refs
移动所选的圆圈;但是,这超出了您最初问题的范围。
最后,我还解决了 styled-components
弃用问题,方法是将 styled.div.data.attr
重组为 function
,returns 为 style
属性 与 CSS
,而不是 object
与 style
属性 是 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;