reactjs - 使用 React Hooks,为什么我的事件处理程序会以不正确的状态触发?
问题描述
我正在尝试div
使用反应钩子创建这个旋转示例的副本。https://codesandbox.io/s/XDjY28XoV
到目前为止,这是我的代码
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [box, setBox] = useState(null);
const [isActive, setIsActive] = useState(false);
const [angle, setAngle] = useState(0);
const [startAngle, setStartAngle] = useState(0);
const [currentAngle, setCurrentAngle] = useState(0);
const [boxCenterPoint, setBoxCenterPoint] = useState({});
const setBoxCallback = useCallback(node => {
if (node !== null) {
setBox(node)
}
}, [])
// to avoid unwanted behaviour, deselect all text
const deselectAll = () => {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
}
// method to get the positionof the pointer event relative to the center of the box
const getPositionFromCenter = e => {
const fromBoxCenter = {
x: e.clientX - boxCenterPoint.x,
y: -(e.clientY - boxCenterPoint.y)
};
return fromBoxCenter;
}
const mouseDownHandler = e => {
e.stopPropagation();
const fromBoxCenter = getPositionFromCenter(e);
const newStartAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
setStartAngle(newStartAngle);
setIsActive(true);
}
const mouseUpHandler = e => {
deselectAll();
e.stopPropagation();
if (isActive) {
const newCurrentAngle = currentAngle + (angle - startAngle);
setIsActive(false);
setCurrentAngle(newCurrentAngle);
}
}
const mouseMoveHandler = e => {
if (isActive) {
const fromBoxCenter = getPositionFromCenter(e);
const newAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
box.style.transform =
"rotate(" +
(currentAngle + (newAngle - (startAngle ? startAngle : 0))) +
"deg)";
setAngle(newAngle)
}
}
useEffect(() => {
if (box) {
const boxPosition = box.getBoundingClientRect();
// get the current center point
const boxCenterX = boxPosition.left + boxPosition.width / 2;
const boxCenterY = boxPosition.top + boxPosition.height / 2;
// update the state
setBoxCenterPoint({ x: boxCenterX, y: boxCenterY });
}
// in case the event ends outside the box
window.onmouseup = mouseUpHandler;
window.onmousemove = mouseMoveHandler;
}, [ box ])
return (
<div className="box-container">
<div
className="box"
onMouseDown={mouseDownHandler}
onMouseUp={mouseUpHandler}
ref={setBoxCallback}
>
Rotate
</div>
</div>
);
}
export default App;
目前 mouseMoveHandler 的调用状态为 ,isActive = false
即使该状态实际上为真。如何让这个事件处理程序以正确的状态触发?
此外,控制台正在记录警告:
React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array react-hooks/exhaustive-deps
为什么我必须在 useEffect 依赖数组中包含组件方法?对于使用 React Hooks 的其他更简单的组件,我从来不需要这样做。
谢谢
解决方案
问题
为什么是
isActive
假的?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(注意为方便起见,我只是在谈论mouseMoveHandler
,但这里的所有内容也适用mouseUpHandler
)
当上面的代码运行时,会创建一个函数实例,它通过函数闭包isActive
拉入变量。该变量是一个常量,因此如果在定义函数时为 false,那么只要该函数实例存在 ,它就会一直存在。isActive
false
useEffect
还接受一个函数,并且该函数具有对您的moveMouseHandler
函数实例的常量引用 - 因此只要存在该 useEffect 回调,它就会引用moveMouseHandler
whereisActive
为 false 的副本。
当更改时,组件会重新渲染,并会在其中创建一个新的isActive
实例。但是,仅在依赖项发生更改时才重新运行其函数 - 在这种情况下,依赖项 ( ) 没有更改,因此不会重新运行并且where为 false 的版本仍然附加到窗口,无论当前状态如何.moveMouseHandler
isActive
true
useEffect
[box]
useEffect
moveMouseHandler
isActive
这就是为什么“exhaustive-deps”钩子会警告你的原因useEffect
——它的一些依赖关系可以改变,而不会导致钩子重新运行和更新这些依赖关系。
修复它
由于钩子间接依赖于isActive
,您可以通过添加isActive
到deps
数组中来解决此问题 for useEffect
:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
但是,这不是很干净:如果您更改mouseMoveHandler
它以使其依赖于更多状态,您将遇到相同的错误,除非您记得也将其添加到deps
数组中。(短绒也不喜欢这样)
该useEffect
函数间接依赖,isActive
因为它直接依赖于mouseMoveHandler
; 因此,您可以将其添加到依赖项中:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
通过此更改,useEffect 将使用新版本重新运行,mouseMoveHandler
这意味着它将尊重isActive
. 但是它会运行得太频繁 - 它每次都会运行mouseMoveHandler
成为一个新的函数实例......这是每次渲染,因为每次渲染都会创建一个新函数。
我们真的不需要在每次渲染时都创建一个新函数,只有在isActive
发生变化时才需要:ReactuseCallback
为该用例提供了钩子。您可以将您的定义mouseMoveHandler
为
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
现在一个新的函数实例仅在isActive
更改时创建,然后将触发useEffect
在适当的时刻运行,并且您可以更改定义mouseMoveHandler
(例如添加更多状态)而不会破坏您的useEffect
钩子。
这可能仍然会给你的钩子带来一个问题useEffect
:它会在每次isActive
更改时重新运行,这意味着它会在每次更改时设置盒子中心点isActive
,这可能是不需要的。您应该将效果拆分为两个单独的效果以避免此问题:
useEffect(() => {
// update box center
}, [box])
useEffect(() => {
// expose window methods
}, [mouseMoveHandler, mouseUpHandler]);
最终结果
最终你的代码应该是这样的:
const mouseMoveHandler = useCallback(e => {
/* ... */
}, [isActive]);
const mouseUpHandler = useCallback(e => {
/* ... */
}, [isActive]);
useEffect(() => {
/* update box center */
}, [box]);
useEffect(() => {
/* expose callback methods */
}, [mouseUpHandler, mouseMoveHandler])
更多信息:
React 作者之一 Dan Abramov 在他的Complete Guide to useEffect 博文中进行了更详细的介绍。
推荐阅读
- amazon-web-services - 从 AWS Lambda 触发 Kubernetes 作业
- android - 从最近清除应用程序时,Workmnager 不会重复
- java - java.lang.Exception:找不到驱动程序
- javascript - 为什么我的 bootstrap.min.css 似乎不起作用?
- snowflake-cloud-data-platform - 雪花验证功能未显示错误
- php - 如何检查复选框的位置?
- visual-studio-code - 如何选择文本编辑器打开文件?
- ios - 如何将左栏按钮项设置为用透明导航栏填充为黑色?
- java - 如何在变量名中使用 int?
- redis - redis 客户端列表显示很多 cmd=auth