6

In the follow example, we have a button that starts and upload, and disables itself while the upload is in progress. It reenables itself when the upload is done.

Is it possible that, due to the asynchronous nature of React's setState, for a user that is clicking really fast, to trigger the onClick callback twice before the button is disabled?

Please do not answer with solutions for how to avoid this scenario, I want to know if this scenario if even possible in the first place and if so, how to reproduce it.

import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      uploadDisabled: false
    }
  }


  upload = () => {
    this.setState({
      uploadDisabled: true
    })

    fetch('/upload').then(() => {
      this.setState({
        uploadDisabled: false
      })
    })
  }

  render () {
    return (
      <div>
        <button disabled={this.state.uploadDisabled} onClick={this.upload}>
          Upload
        </button>
      </div>
    )
  }
}
Transcendence
  • 2,616
  • 3
  • 21
  • 31

6 Answers6

2

I'm not sure it's possible:

class Button extends React.Component {
  constructor(){
    super();
    this.state = {disabled: false};
    this.onClick = () => {
      this.setState({
        disabled: true
      });
      console.log('click')

      setTimeout(() => {
        this.setState({
          disabled: false
        });
      }, 1000)
    };
  }

  render() {
    return <button onClick={this.onClick} disabled={this.state.disabled}>foo</button>;
  }
}

ReactDOM.render(
  <Button/>,
  document.getElementById('container')
);

setTimeout(() => {
  const button = document.getElementsByTagName('button')[0]
  for(let i = 0; i < 2; i++) {
    button.click()
  }
}, 200)

https://jsbin.com/tamifaruqu/edit?html,js,console,output

It prints click only once

iofjuupasli
  • 3,818
  • 1
  • 16
  • 17
1

I have been experimenting a bit and was wondering whether my solution could do the trick. I define a boolean property on the component that is not on the state and which is toggled directly on button click. Here is the code:

class App extends React.Component {
  // not on the state, will be toggled directly when the user presses the button
  uploadInProgress = false;    
  // just for the purpose of experiment, has nothing to do with the solution
  uploadCalledCount = 0;    
  state = {
     uploadDisabled: false
  }
  upload = () => {
    if (this.uploadInProgress) return;

    this.uploadInProgress = true;
    this.uploadCalledCount++;

    // let's experiment and make setState be called after 1 second
    // to emulate some delay for the purpose of experiment
    setTimeout(() => {
      this.setState({ uploadDisabled: true })
    }, 1000);

    // emulate a long api call, for the purpose of experiment
    setTimeout(() => {
      this.setState(
          { uploadDisabled: false }, 
          () => { this.uploadInProgress = false; })
    }, 3000);
  }

  render() {
    return (
      <div>
        <button disabled={this.state.uploadDisabled} onClick={this.upload}>
          Upload
        </button>
        <br />
        {this.uploadCalledCount}
      </div>)
    }
}

Here is a working example om codesandbox.

The way to check: click the button as many times as an anxious and impatient user would do, the button will get disabled after one second delay set before setState call, then the number of actual calls of the upload function will appear on the screen (after state changes), then the button gets enabled after 3 second delay again.

margaretkru
  • 2,751
  • 18
  • 20
0
    import debounce from "lodash/debounce"
//import debounce at the top
    upload = () => {
        this.setState({
          uploadDisabled: true
        })

        fetch('/upload').then(() => {
          this.setState({
            uploadDisabled: false
          })
        })
      }
    onClickUpload = () => {
      _debounce(upload, time) // time is the number of milliseconds to delay.
    }
   render () {
    return (
      <div>
        <button disabled={this.state.uploadDisabled} onClick={this.onClickUpload}>
          Upload
        </button>
      </div>
    )
  }

This may help

simbathesailor
  • 3,681
  • 2
  • 19
  • 30
0

Simplest way is to use _.debounce(func, [wait=0], [options={}]) by lodash This function ignore the event triggers for 'wait' period of time

RohanS404
  • 41
  • 1
  • 8
0

It is just an idea but maybe you could work with onMouseDown and onMouseUp instead of just onClick. Also React's event system provides an event onDoubleClick, so maybe you could try to intercept with it.

You can find more infos about React's events here: https://reactjs.org/docs/events.html#mouse-events

SeBe
  • 926
  • 3
  • 9
  • 13
0

You can create a ref for the button, and use its ref to either enable or disable it.

<button ref="btn" onClick={this.onClick}>Send</button>

And in the onclick, you can disable it

this.refs.btn.setAttribute("disabled", "disabled");

And to reenable, do

this.refs.btn.removeAttribute("disabled");

Reference link

raksheetbhat
  • 850
  • 2
  • 11
  • 25
  • this does answer the question nor does it provide an example for how to reproduce the scenario described in the question – Transcendence Jan 04 '18 at 19:22