使用 React Hooks,为什么我的事件处理程序会以错误的状态触发?
Using React Hooks, why are my event handlers firing with the incorrect state?
我正在尝试使用 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
,尽管该状态实际上为 true。我怎样才能让这个事件处理程序以正确的状态触发?
此外,控制台正在记录警告:
React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array react-hooks/exhaustive-deps
为什么我必须在 useEffect 依赖数组中包含组件方法?对于使用 React Hooks 的其他更简单的组件,我从来没有这样做过。
谢谢
。
看来你有类似的问题。基本问题是 "it gets its value from the closure where it was defined"
试试解决方案 2 "Use a ref"。在你的场景中
在下面添加 useRef 和 useEffect
let refIsActive = useRef(isActive);
useEffect(() => {
refIsActive.current = isActive;
});
然后在 mouseMoveHandler 中,使用 ref
const mouseMoveHandler = (e) => {
console.log('isActive',refIsActive.current);
if (refIsActive.current) {
问题
Why is isActive
false?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(为方便起见,我只谈论 mouseMoveHandler
,但这里的所有内容也适用于 mouseUpHandler
)
当上面的代码运行s时,创建了一个函数实例,通过function closure拉入了isActive
变量。该变量是一个常量,因此如果在定义函数时 isActive
为假,那么只要该函数实例存在,它 总是 将是 false
。
useEffect
也接受一个函数,并且该函数对您的 moveMouseHandler
函数实例有一个常量引用 - 所以只要 useEffect 回调存在,它就会引用 [=23= 的副本] 其中 isActive
为假。
当 isActive
更改时,组件将重新呈现,并且将创建 moveMouseHandler
的新实例,其中 isActive
为 true
。但是,useEffect
仅在依赖关系发生变化时重新运行 其功能 - 在这种情况下,依赖关系 ([box]
) 没有变化,因此 useEffect
不会重新-运行 和 moveMouseHandler
的版本(其中 isActive
为假)仍然附加到 window,无论当前状态如何。
这就是 "exhaustive-deps" 挂钩警告您有关 useEffect
的原因 - 它的某些依赖项可以更改,而不会导致挂钩重新 运行 并更新这些依赖项。
正在修复
由于挂钩间接依赖于 isActive
,您 可以 通过将 isActive
添加到 [=22 的 deps
数组来解决此问题=]:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
但是,这不是很干净:如果您更改 mouseMoveHandler
使其依赖于更多状态,您将遇到相同的错误,除非您记得来将其添加到 deps
数组也是如此。 (linter 也不喜欢这样)
useEffect
函数间接依赖isActive
,因为它直接依赖mouseMoveHandler
;因此,您可以将其添加到依赖项中:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
通过此更改,useEffect 将重新 运行 与 mouseMoveHandler
的新版本,这意味着它会尊重 isActive
。 但是它会运行太频繁了 - 它会运行每次mouseMoveHandler
成为一个新的函数实例......这是每一个渲染,因为每次渲染都会创建一个新函数。
我们真的不需要在每次渲染时都创建一个新函数,只有当 isActive
发生变化时才需要:React 为该用例提供了 useCallback
挂钩。您可以将 mouseMoveHandler
定义为
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
现在只有在isActive
变化时才创建一个新的函数实例,然后会在适当的时候触发useEffect
到运行,你可以改变[的定义=17=](例如添加更多状态)而不破坏你的 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])
更多信息:
Dan Abramov,React 的作者之一,在他的 Complete Guide to useEffect 博文中进行了更详细的介绍。
我已经创建了一个 NPM 模块来解决它 https://www.npmjs.com/package/react-usestateref
它似乎可以帮助并回答你如何触发当前状态的问题。
它是 useState 和 useRef 的组合,让你像 ref
一样得到最后一个值
使用示例:
const [isActive, setIsActive,isActiveRef] = useStateRef(false);
console.log(isActiveRef.current)
更多信息:
我正在尝试使用 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
,尽管该状态实际上为 true。我怎样才能让这个事件处理程序以正确的状态触发?
此外,控制台正在记录警告:
React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array react-hooks/exhaustive-deps
为什么我必须在 useEffect 依赖数组中包含组件方法?对于使用 React Hooks 的其他更简单的组件,我从来没有这样做过。
谢谢
试试解决方案 2 "Use a ref"。在你的场景中
在下面添加 useRef 和 useEffect
let refIsActive = useRef(isActive);
useEffect(() => {
refIsActive.current = isActive;
});
然后在 mouseMoveHandler 中,使用 ref
const mouseMoveHandler = (e) => {
console.log('isActive',refIsActive.current);
if (refIsActive.current) {
问题
Why is
isActive
false?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(为方便起见,我只谈论 mouseMoveHandler
,但这里的所有内容也适用于 mouseUpHandler
)
当上面的代码运行s时,创建了一个函数实例,通过function closure拉入了isActive
变量。该变量是一个常量,因此如果在定义函数时 isActive
为假,那么只要该函数实例存在,它 总是 将是 false
。
useEffect
也接受一个函数,并且该函数对您的 moveMouseHandler
函数实例有一个常量引用 - 所以只要 useEffect 回调存在,它就会引用 [=23= 的副本] 其中 isActive
为假。
当 isActive
更改时,组件将重新呈现,并且将创建 moveMouseHandler
的新实例,其中 isActive
为 true
。但是,useEffect
仅在依赖关系发生变化时重新运行 其功能 - 在这种情况下,依赖关系 ([box]
) 没有变化,因此 useEffect
不会重新-运行 和 moveMouseHandler
的版本(其中 isActive
为假)仍然附加到 window,无论当前状态如何。
这就是 "exhaustive-deps" 挂钩警告您有关 useEffect
的原因 - 它的某些依赖项可以更改,而不会导致挂钩重新 运行 并更新这些依赖项。
正在修复
由于挂钩间接依赖于 isActive
,您 可以 通过将 isActive
添加到 [=22 的 deps
数组来解决此问题=]:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
但是,这不是很干净:如果您更改 mouseMoveHandler
使其依赖于更多状态,您将遇到相同的错误,除非您记得来将其添加到 deps
数组也是如此。 (linter 也不喜欢这样)
useEffect
函数间接依赖isActive
,因为它直接依赖mouseMoveHandler
;因此,您可以将其添加到依赖项中:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
通过此更改,useEffect 将重新 运行 与 mouseMoveHandler
的新版本,这意味着它会尊重 isActive
。 但是它会运行太频繁了 - 它会运行每次mouseMoveHandler
成为一个新的函数实例......这是每一个渲染,因为每次渲染都会创建一个新函数。
我们真的不需要在每次渲染时都创建一个新函数,只有当 isActive
发生变化时才需要:React 为该用例提供了 useCallback
挂钩。您可以将 mouseMoveHandler
定义为
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
现在只有在isActive
变化时才创建一个新的函数实例,然后会在适当的时候触发useEffect
到运行,你可以改变[的定义=17=](例如添加更多状态)而不破坏你的 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])
更多信息:
Dan Abramov,React 的作者之一,在他的 Complete Guide to useEffect 博文中进行了更详细的介绍。
我已经创建了一个 NPM 模块来解决它 https://www.npmjs.com/package/react-usestateref
它似乎可以帮助并回答你如何触发当前状态的问题。
它是 useState 和 useRef 的组合,让你像 ref
使用示例:
const [isActive, setIsActive,isActiveRef] = useStateRef(false);
console.log(isActiveRef.current)
更多信息: