在 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 null
和 close 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} />
</>
);
}
让我先解释一下我的代码的目标。我有一个名为“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 null
和 close 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} />
</>
);
}