如何使用 React Hooks 处理 React Svg 拖放
How to handle React Svg Drag and Drop with React Hooks
我正在尝试在 SVG 形状上执行拖放操作。我成功地使用 Class 组件使其工作。这是代码沙箱的 link:https://codesandbox.io/s/qv81pq1roq
但现在我想通过使用新的 React api 和自定义 Hook 来提取此逻辑,该 Hook 将能够将此功能添加到功能组件中。我尝试了很多东西,但没有任何效果。这是我最后一次尝试:
https://codesandbox.io/s/2x2850vjk0
我怀疑我添加和删除事件侦听器的方式...所以这是我的问题:
您认为,可以将此 DnD SVG 逻辑放入自定义 Hook 中吗?如果是,你知道我做错了什么吗?
我在此处修复了示例 - https://codesandbox.io/s/2w0oy6qnvn
您的挂钩示例中存在许多问题:
setPosition
不同于 setState
。它不进行浅合并,它用新值替换整个对象,因此您必须使用 Object.assign()
或扩展运算符与以前的值合并。此外,setPosition()
挂钩采用回调值,如果您在设置新值时需要引用它,它将提供先前状态值作为第一个参数。
与 类 不同,handleMouseMove
函数在每次渲染时都会重新创建,因此 document.removeEventListener('mousemove', handleMouseMove)
不再引用初始 handleMouseMove
值document.addEventListener('mousemove', handleMouseMove)
被调用。解决方法是使用 useRef
创建一个对象,该对象在组件的整个生命周期中持续存在,非常适合保留对函数的引用。
handleMouseDown
中的事件参数与您在 setPosition
中引用的事件参数不同。因为 React 使用事件池并重用事件,所以 setPosition
中的事件可能已经不同于传递到 handleMouseDown
中的事件。解决这个问题的方法是先获取 pageX
和 pageY
的值,这样在 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>
这是我一直用来在 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
}
我正在尝试在 SVG 形状上执行拖放操作。我成功地使用 Class 组件使其工作。这是代码沙箱的 link:https://codesandbox.io/s/qv81pq1roq
但现在我想通过使用新的 React api 和自定义 Hook 来提取此逻辑,该 Hook 将能够将此功能添加到功能组件中。我尝试了很多东西,但没有任何效果。这是我最后一次尝试:
https://codesandbox.io/s/2x2850vjk0
我怀疑我添加和删除事件侦听器的方式...所以这是我的问题:
您认为,可以将此 DnD SVG 逻辑放入自定义 Hook 中吗?如果是,你知道我做错了什么吗?
我在此处修复了示例 - https://codesandbox.io/s/2w0oy6qnvn
您的挂钩示例中存在许多问题:
setPosition
不同于setState
。它不进行浅合并,它用新值替换整个对象,因此您必须使用Object.assign()
或扩展运算符与以前的值合并。此外,setPosition()
挂钩采用回调值,如果您在设置新值时需要引用它,它将提供先前状态值作为第一个参数。与 类 不同,
handleMouseMove
函数在每次渲染时都会重新创建,因此document.removeEventListener('mousemove', handleMouseMove)
不再引用初始handleMouseMove
值document.addEventListener('mousemove', handleMouseMove)
被调用。解决方法是使用useRef
创建一个对象,该对象在组件的整个生命周期中持续存在,非常适合保留对函数的引用。handleMouseDown
中的事件参数与您在setPosition
中引用的事件参数不同。因为 React 使用事件池并重用事件,所以setPosition
中的事件可能已经不同于传递到handleMouseDown
中的事件。解决这个问题的方法是先获取pageX
和pageY
的值,这样在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>
这是我一直用来在 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
}