使用React Hooks,为什么我的事件处理程序以错误的状态触发?

时间:2019-04-26 20:51:14

标签: reactjs event-listener react-hooks react-lifecycle react-lifecycle-hooks

我正在尝试使用react钩子创建此旋转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;

当前,即使状态为true,也会以状态isActive = false调用mouseMoveHandler。如何使该事件处理程序以正确的状态启动?

此外,控制台正在记录警告:

React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array  react-hooks/exhaustive-deps

为什么必须在useEffect依赖项数组中包括组件方法?我从来没有必要使用React Hooks对其他更简单的组件执行此操作。

谢谢

3 个答案:

答案 0 :(得分:1)

我创建了一个 NPM 模块来解决它 https://www.npmjs.com/package/react-usestateref,它似乎可以帮助您回答如何触发当前状态的问题。

它是 useState 和 useRef 的组合,让你像 ref 一样获取最后一个值

使用示例:

    const [isActive, setIsActive,isActiveRef] = useStateRef(false);
    console.log(isActiveRef.current)

更多信息:

答案 1 :(得分:0)

React Hooks useState+useEffect+event gives stale state。 看来您遇到类似的问题。基本问题是“它从定义它的闭包中获得价值”

尝试解决方案2“使用参考” 。在您的情况下

useRef useEffect

下面添加
let refIsActive = useRef(isActive);
useEffect(() => {
    refIsActive.current = isActive;
});

然后在mouseMoveHandler中,使用该引用

 const mouseMoveHandler = (e) => {    
  console.log('isActive',refIsActive.current);
    if (refIsActive.current) {

答案 2 :(得分:0)

问题

  

为什么isActive为假?

const mouseMoveHandler = e => {
   if(isActive) {
       // ...
   }
};

(为方便起见,我只在谈论mouseMoveHandler,但这里的所有内容也适用于mouseUpHandler

上面的代码运行时,将创建一个函数实例,该函数实例通过function closure引入isActive变量。该变量是一个常量,因此,如果在定义函数时isActive为假,则只要该函数实例存在,它总是false

useEffect也带有一个函数,并且该函数具有对您的moveMouseHandler函数实例的常量引用-只要存在useEffect回调,它就会引用moveMouseHandler的副本,其中isActive是错误的。

isActive发生更改时,组件将重新渲染,并且将创建moveMouseHandlerisActive的新true实例。但是,useEffect仅在依赖项已更改的情况下重新运行其功能-在这种情况下,依赖项([box])尚未更改,因此useEffect不会重新运行,并且不管当前状态如何,moveMouseHandler isActive为假的useEffect仍附加在窗口上。

这就是为什么“ exhaustive-deps”挂钩警告您有关isActive的原因-它的某些依赖项可以更改,而不会导致挂钩重新运行并更新这些依赖项。


修复

由于钩子间接依赖于isActive,因此您可以通过将deps添加到useEffect的{​​{1}}数组中来解决此问题:

// Works, but not the best solution
useEffect(() => {
    //...
}, [box, isActive])

但是,这不是很干净:如果您更改mouseMoveHandler使其依赖于更多状态,除非您记得将其添加到{{1}中,否则您将遇到相同的错误。 }数组。 (另外,短绒棉也不会这样)

deps函数间接依赖于useEffect,因为它直接依赖于isActive;因此,您可以将其添加到依赖项中:

mouseMoveHandler

进行此更改后,useEffect将使用新版本的useEffect(() => { //... }, [box, mouseMoveHandler]) 重新运行,这意味着它将尊重mouseMoveHandler但是它会经常运行-每当isActive成为一个新的函数实例时,它将运行......是每个渲染器,因为每个渲染器都会创建一个新函数。 / p>

我们真的不需要为每个渲染创建一个新函数,仅当mouseMoveHandler发生变化时:React为该用例提供了isActive钩子。您可以将useCallback定义为

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]);

更多信息:

React的作者之一丹·阿布拉莫夫(Dan Abramov)在他的Complete Guide to useEffect博客中详细介绍了