0

There is a save button in my react application that takes several seconds to complete it's onClick handler. In the time when the onClick handler is running, the user can press the button again and the onClick handler will run again after the last run is finished, saving a duplicate of the original data.

I've seen many simple solutions on here but none match my case, here's why:

  1. My button must automatically re-enable once the onClick handler finishes.
  2. The save operation takes several seconds, so debouncing is not ideal as we'd have to debounce the button for several seconds as well.
  3. The button class is being called from another class, and is being passed an onClick handler as a prop.

This is a legacy application, these features are fundamental to the structure and cannot be changed without re-writing the entire application.

As the application this button belongs to is large, I've abstracted it into a model comprised of only two classes: Button, and App (calls Button and passes onClick handler). The passed onClick handler has a loop creating/deleting local variables to mock the lengthy save function of the original.

import "./App.css";
import React, { Component } from "react";
import Button from "./components/button";

class App extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.state = {
      counter: 0,
      btnLock: false,
    };
  }

  async handleClick()  {
    await this.setState({ counter: this.state.counter + 1, btnLock: true }, () => {
      var i = 0;
      var text;
      while (i < 20000000) {
        text = "The number is " + i;
        i++;
      }
    });
    await console.log("click" + this.state.counter);
    await this.setState({btnLock: false})
  };

  render() {
    return <Button handleClick={this.handleClick}
    btnLock={this.state.btnLock}/>;
  }
}

export default App;

import React, { Component } from "react";

class Button extends Component {
  constructor(props){
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    if (!this.props.btnLock) {
     this.props.handleClick();
  }};

  render() {
      return <button onClick={this.handleClick}>BUTTON</button>;
  }
}

export default Button;

currently this behaves exactly like the legacy application, where clicks to this button will queue while onClick is running. How do I disable the button or block execution of the passed onClick function while onClick is running?

  • You'll want to do 2 things: set some disabled state to true at the top of handleClick, and set it back to false in a `finally` block. Also, throttle the handleClick function so that any double presses before react re-renders are ignored. – windowsill Oct 18 '22 at 23:53

1 Answers1

0

You could achieve the synchronous behavior you're looking for by using a ref to do the locking, but still have the ui behave asynchronously.

const BlockingButton = ({onClick, render}) => {
  const [disabledView, setDisabledView] = useState(false);
  const disabledSync = useRef(false);

  const _onClick = () => {
    if (disabledSync.current || disabledView) return;

    disabledSync.current = true;
    setDisabledView(true);

    try {
      await onClick();
    } finally {
      disabledSync.current = false;
      setDisabledView(false);
    }

  }; 

return <button disabled={disabledView} onClick={_onClick}>{render()}</button>;
};
windowsill
  • 3,599
  • 1
  • 9
  • 14