【问题标题】:Using React Hooks, why are my event handlers firing with the incorrect state?使用 React Hooks,为什么我的事件处理程序会以不正确的状态触发?
【发布时间】:2025-12-27 10:10:17
【问题描述】:

我正在尝试使用反应钩子创建这个旋转的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 的其他更简单的组件,我从来不需要这样做。

谢谢

【问题讨论】:

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


    【解决方案1】:

    问题

    为什么isActive 是假的?

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

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

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

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

    isActive 发生变化时,组件会重新渲染,并会创建一个moveMouseHandler 的新实例,其中isActivetrue。但是,useEffect 仅在依赖项已更改时才重新运行其功能 - 在这种情况下,依赖项 ([box]) 没有更改,因此 useEffect 不会重新运行,moveMouseHandler 的版本@987654342 @ 为 false 仍附加到窗口,无论当前状态如何。

    这就是为什么“exhaustive-deps”钩子会警告您useEffect - 它的一些依赖项可以更改,而不会导致钩子重新运行并更新这些依赖项。


    修复它

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

    // 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在适当的时候运行,并且你可以在不中断的情况下更改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])
    

    更多信息:

    Dan Abramov,React 的作者之一,在他的 Complete Guide to useEffect 博文中进行了更详细的介绍。

    【讨论】:

      【解决方案2】:

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

      尝试解决方案 2“使用引用”。在你的场景中

      useRef下方添加,useEffect

      let refIsActive = useRef(isActive);
      useEffect(() => {
          refIsActive.current = isActive;
      });
      

      然后在 mouseMoveHandler 中,使用那个 ref

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

      【讨论】:

        【解决方案3】:

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

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

        使用示例:

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

        更多信息:

        【讨论】:

          最近更新 更多