当前位置:优学网  >  在线题库

React如何禁用按钮并在事件处理完后启用按钮?

发表时间:2022-05-31 15:59:18 阅读:41

我想用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 输出都是 falseflushSync没有起到立即更新 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 属性,又迅速地消失了。
已经无计可施了,网络上也找不到答案。
能帮我解答下吗?

🎖️ 优质答案
  • 这种现象应该与react无关,使用原生js也是一样的效果

    <div id="app">
        <button id="btn">btn</button>
    </div>
    var btn = document.getElementById('btn')
    
    btn.addEventListener('click',()=>{
      btn.setAttribute('disabled',true)
      for(let i=0;i<1000;i++){
          console.log('handler')
      }
      btn.removeAttribute('disabled')
    })

    应该是你那个for循环阻塞了浏览器渲染

    老感觉你那个async修饰的handleClick不太对。await后面的promise和同步没什么区别。你要是非要用for循环阻塞主线程来实现延迟,可以这样做

    btn.addEventListener('click',()=>{
      btn.setAttribute('disabled',true)
    
      setTimeout(()=>{
        const now = Date.now()
        for(let i=0;i<100000;i++){
          console.log('handler')
        }
        console.log(Date.now() - now)
    
        btn.removeAttribute('disabled')
      },0)
    
    })

    注意看控制台,disabled效果的时长取决于for循环的耗时。虽然handker log的很慢,但实际时间不到一秒(我这边是这样,浏览器卡死别怪我)。

  • 如果 clickEvent 是同步的话,根本不需要 loading 状态,因为处理事件的这段时间里浏览器是阻塞的,按钮点不了。

    如果是异步的,那么要在这个异步事件确定结束之后(比如用回调函数通知) 再把 loading 改回去。
    这就好比男人要上前线打仗,妻子在这期间吃素斋戒,总得要等到前线来的消息(荣归 or 捐躯)才能开荤,总不能前脚把人送出家门(调用函数,还不知道函数执行结果)后脚就啃猪蹄吧。

  • 可以尝试使用一个延迟函数,点击事件,函数运行,按钮禁用,处理事件clickEvent,事件结束后,500毫秒之后再把按钮恢复。
    重点是保证在事件处理结束之后才能进行状态修改,是不是可以设置更长的时间,或者监听事件处理的时间,然后相对应的在完成之后再进行修改。

    function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
      setLoading(true);
      clickEvent(event);
      setTimeout(() => {
        setLoading(false);
      }, 500);      
    }

    已参与了 SegmentFault 思否社区 10 周年「问答」打卡 ,欢迎正在阅读的你也加入。

  • 相关问题