我想用React 18实现一个按钮组件,在点击时将按钮禁用,处理完事件后自动按钮启用。我想这是一个很简单的功能,初始代码如下:
import React, { useState } from "react";
export function Button(props: any) {
const { children, ...properties } = props;
const clickEvent: React.MouseEventHandler<HTMLButtonElement> = props.onClick;
const [loading, setLoading] = useState(false);
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
setLoading(true);
clickEvent(event);
setLoading(false);
}
return (
<button disabled={loading} onClick={handleClick}>
{children}
</button>
);
}
并在调用方写了一个耗时的事件来测试:
import { Button } from "./button";
function App() {
return (
<div>
<Button
onClick={() => {
for (let index = 0; index < 1000; index++) {
console.log("handling...");
}
}}
>
按钮
</Button>
</div>
);
}
预料中调用了 setLoading(true)
后重绘,按钮被禁用,处理事件完后调用 setLoading(false)
后重绘,按钮被启用。
但是不起效果,点击后按钮没有被禁用,但是控制台一直有 handling...
输出。
我在网上搜索后,了解到 react 18 会合并 useState
的更新,所以要用 flushSync
包起来,我改为了以下写法:
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
flushSync(() => setLoading(true));
console.log(loading);
clickEvent(event);
flushSync(() => setLoading(false));
console.log(loading);
}
但是仍然没有效果,甚至是里面两个 log 输出都是 false
,flushSync
没有起到立即更新 DOM 的效果。在事件执行完后,我在 F12 里看到按钮元素迅速地出现了 disabled
又迅速地消失了。仍然没有预期的效果。
我猜想可能是因为我传进的 for 循环阻塞了,所以尝试了以下的写法,将 for 循环放进队列里以免阻塞:
async function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
flushSync(() => setLoading(true));
console.log(loading);
await new Promise<void>((resovle) => {
clickEvent(event);
flushSync(() => setLoading(false));
console.log(loading);
resovle();
});
}
仍然是一样的效果,不起作用,在 F12 里看到按钮元素在 for 循环执行完后,迅速地出现了 disabled
属性,又迅速地消失了。
已经无计可施了,网络上也找不到答案。
能帮我解答下吗?
如果 clickEvent 是同步的话,根本不需要 loading
状态,因为处理事件的这段时间里浏览器是阻塞的,按钮点不了。
如果是异步的,那么要在这个异步事件确定结束之后(比如用回调函数通知) 再把 loading
改回去。
这就好比男人要上前线打仗,妻子在这期间吃素斋戒,总得要等到前线来的消息(荣归 or 捐躯)才能开荤,总不能前脚把人送出家门(调用函数,还不知道函数执行结果)后脚就啃猪蹄吧。
可以尝试使用一个延迟函数,点击事件,函数运行,按钮禁用,处理事件clickEvent,事件结束后,500毫秒之后再把按钮恢复。
重点是保证在事件处理结束之后才能进行状态修改,是不是可以设置更长的时间,或者监听事件处理的时间,然后相对应的在完成之后再进行修改。
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
setLoading(true);
clickEvent(event);
setTimeout(() => {
setLoading(false);
}, 500);
}
已参与了 SegmentFault 思否社区 10 周年「问答」打卡 ,欢迎正在阅读的你也加入。
这种现象应该与react无关,使用原生js也是一样的效果
应该是你那个for循环阻塞了浏览器渲染
老感觉你那个async修饰的handleClick不太对。await后面的promise和同步没什么区别。你要是非要用for循环阻塞主线程来实现延迟,可以这样做
注意看控制台,disabled效果的时长取决于for循环的耗时。虽然handker log的很慢,但实际时间不到一秒(我这边是这样,浏览器卡死别怪我)。