在 render 方法中使用事件处理程序时如何管理异步状态更新?

How to manage asynchronous state updates when using event handlers in render method?

让我先解释一下我的代码的目标。我有一个名为“Tile”的反应组件,其中包含一个名为“TileMenu”的子组件,当我右键单击我的 Tile,调用函数“openMenu”时,它会显示出来。我想有两种关闭它的方法:

但是,我还希望它在鼠标悬停在其上时保持原位。所以我需要一个函数来取消定时器,我称之为“keepMenuOpened”。如果我将鼠标移开,则会再次调用 openMenu() 以重新启动计时器。

这是我的代码:

import TileMenu from './TileMenu'

function Tile() {


  const [openedMenu, setOpenedMenu] = useState(false);
    // state used to display —or not— the TileMenu component
  const [timeoutID, setTimeoutID] = useState(null);
    // state to store timeout ID and clear it


  function openMenu() {
    // Actually open TileMenu
    setOpenedMenu(true);

    // Prepare TileMenu closing
    window.onclick = closeMenu;
      // first case: click somewhere else
    setTimeoutID(setTimeout(closeMenu, 3000));
      // second case: time out
    console.log('open', timeoutID);
  }

  function closeMenu() {
    setOpenedMenu(false);

    window.onclick = null;
    console.log('close', timeoutID);
    clearTimeout(timeoutID);
  }

  function keepMenuOpened() {
    console.log('keep', timeoutID);
    clearTimeout(timeoutID);
  }


  return(
    <>
      {openedMenu &&
      <TileMenu
        onMouseOver={keepMenuOpened} onMouseLeave={openMenu} // These two props are passed on to TileMenu component
      />}

      <textarea
        onContextMenu={openMenu}
      >
      </textarea>
    </>
  );
}

export default Tile

起初,它似乎工作得很好。但是我注意到,当我打开,然后手动关闭,最后再次打开我的TileMenu时,第二次(单独这次)关闭的延迟是从我第一次打开开始计算的。

我使用 console.log() 来查看幕后发生的事情,这似乎是由 React 中状态的异步更新引起的(事实上,在第一次尝试时,我得到 open nullclose null 在控制台中。当我将鼠标移到 TileMenu 上然后离开它时,我得到例如 open 53,然后是 keep 89,然后是 open 89!)如果我很了解我的具体情况,React 在 openMenu 和 closeMenu 中使用以前的状态,但在 keepMenuOpened 中使用当前状态。

事实上,这不是我的第一次尝试,在使用反应状态之前,“timeoutID”是一个简单的变量。但是这一次,即使在 Tile() 范围内声明并且在 openMenu 和 closeMenu 中可以访问,它在 keepMenuOpened 内部也是不可访问的(它在控制台中记录 keep undefined)。我认为这是因为 closeMenu 是从 openMenu 调用的。我在网上发现它被称为闭包,但我没有弄清楚它是如何与 React 一起工作的。

现在我还没有想出如何解决我的具体问题。我发现我可以使用 useEffect() 来访问我的更新状态,但在我需要在 Tile() 中声明我的函数以将它们用作事件处理程序的情况下,它不起作用。我想知道我的代码是否设计正确。

您需要在 openMenu 调用时清除之前的计时器。

function openMenu() {
  // clear previous timer before open
  clearTimeout(timeoutID);

  // Actually open TileMenu
  setOpenedMenu(true);

  // Prepare TileMenu closing
  window.onclick = closeMenu;

  // first case: click somewhere else
  setTimeoutID(setTimeout(closeMenu, 3000));
  // second case: time out
  console.log('open', timeoutID);
}

function closeMenu() {
  setOpenedMenu(false);

  window.onclick = null;
  console.log('close', timeoutID);

  // timer callback has executed, can remove this line
  clearTimeout(timeoutID);
}

这里的问题是打开菜单时没有重置。

您可能不应该将计时器 ID 存储在状态中,这似乎没有必要。您也不会在组件卸载时清除任何 运行 超时,如果您稍后将状态更新或其他副作用加入队列(假设组件仍处于安装状态),这有时会导致问题。

直接改变 window.click 属性 也被认为是不合适的,您应该添加和删除事件侦听器。

您可以使用 useEffect 挂钩来处理超时的清除 删除 window 在组件卸载时单击清除函数中的事件侦听器。

function Tile() {
  const [openedMenu, setOpenedMenu] = useState(false);
  const timerIdRef = useRef();

  useEffect(() => {
    return () => {
      window.removeEventListener('click', closeMenu);
      clearTimeout(timerIdRef.current);
    }
  }, []);

  function openMenu() {
    setOpenedMenu(true);
    window.addEventListener('click', closeMenu);
    timerIdRef.current = setTimeout(closeMenu, 3000);
  }

  function closeMenu() {
    setOpenedMenu(false);
    window.removeEventListener('click', closeMenu);
    clearTimeout(timerIdRef.current);
  }

  function keepMenuOpened() {
    clearTimeout(timerIdRef.current);
  }

  return(
    <>
      {openedMenu && (
        <TileMenu
          onMouseOver={keepMenuOpened}
          onMouseLeave={openMenu}
        />
      )}

      <textarea onContextMenu={openMenu} />
    </>
  );
}