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

Jon*_*n_B 5 event-listener reactjs react-lifecycle react-hooks react-lifecycle-hooks

我正在尝试div使用 React 钩子创建此旋转示例的副本。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;

Run Code Online (Sandbox Code Playgroud)

当前 mouseMoveHandler 的调用状态为 ,isActive = false即使状态实际上为真。我怎样才能让这个事件处理程序以正确的状态触发?

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

React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array  react-hooks/exhaustive-deps
Run Code Online (Sandbox Code Playgroud)

为什么我必须在 useEffect 依赖数组中包含组件方法?对于使用 React Hooks 的其他更简单的组件,我从来没有这样做过。

谢谢

Ret*_*sam 9

问题

为什么是isActive假的?

const mouseMoveHandler = e => {
   if(isActive) {
       // ...
   }
};
Run Code Online (Sandbox Code Playgroud)

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

当上面的代码运行时,会创建一个函数实例,它isActive通过函数闭包拉入变量。该变量是一个常数,因此,如果isActive是被定义的功能时,那么它的错误总是将是false只要该函数实例存在。

useEffect还需要一个函数,并且该函数具有对您的moveMouseHandler函数实例的常量引用- 因此只要存在该 useEffect 回调,它就会引用moveMouseHandlerwhereisActive为 false的副本。

isActive发生变化时,部件重新渲染,和的新实例moveMouseHandler将在其中创建的isActivetrue。但是,useEffect只有在依赖项发生变化时才重新运行其函数 - 在这种情况下,依赖项 ( [box]) 没有发生变化,因此useEffect不会重新运行并且moveMouseHandlerwhereisActive为 false的版本仍然附加到窗口,无论当前状态如何.

这就是“exhaustive-deps”钩子警告你的原因useEffect——它的一些依赖项可以改变,而不会导致钩子重新运行和更新这些依赖项。


修复它

由于挂钩间接依赖于isActive,您可以通过将以下内容添加isActivedeps数组中来解决此问题useEffect

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

但是,这不是很干净:如果您更改mouseMoveHandler以使其依赖于更多状态,那么您将遇到相同的错误,除非您记得也将其添加到deps数组中。(而且短绒不会喜欢这个)

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

useEffect(() => {
    //...
}, [box, mouseMoveHandler])
Run Code Online (Sandbox Code Playgroud)

通过此更改,useEffect 将使用新版本重新运行,mouseMoveHandler这意味着它将尊重isActive. 然而,它会运行得太频繁——它每次都会运行mouseMoveHandler成为一个新的函数实例......这是每一次渲染,因为每次渲染都会创建一个新函数。

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

const mouseMoveHandler = useCallback(e => {
   if(isActive) {
       // ...
   }
}, [isActive])
Run Code Online (Sandbox Code Playgroud)

现在只有在isActive更改时才会创建一个新的函数实例,然后会useEffect在适当的时刻触发运行,并且您可以在mouseMoveHandler不破坏useEffect钩子的情况下更改(例如添加更多状态)的定义。


这可能仍然会给您的useEffect钩子带来问题:每次isActive更改时它都会重新运行,这意味着每次更改时它都会设置框中心点isActive,这可能是不需要的。您应该将效果拆分为两个单独的效果以避免此问题:

useEffect(() => {
    // update box center
}, [box])

useEffect(() => {
   // expose window methods
}, [mouseMoveHandler, mouseUpHandler]);
Run Code Online (Sandbox Code Playgroud)

最终结果

最终你的代码应该是这样的:

const mouseMoveHandler = useCallback(e => {
    /* ... */
}, [isActive]);

const mouseUpHandler = useCallback(e => {
    /* ... */
}, [isActive]);

useEffect(() => {
   /* update box center */
}, [box]);

useEffect(() => {
   /* expose callback methods */
}, [mouseUpHandler, mouseMoveHandler])
Run Code Online (Sandbox Code Playgroud)

更多信息:

丹·阿布拉莫夫(Dan Abramov)是 React 的作者之一,在他的useEffect 完整指南博客文章中详细介绍了更多细节。