89

In my React component I have a button meant to send some data over AJAX when clicked. I need to happen only the first time, i.e. to disable the button after its first use.

How I'm trying to do this:

var UploadArea = React.createClass({

  getInitialState() {
    return {
      showUploadButton: true
    };
  },

  disableUploadButton(callback) {
    this.setState({ showUploadButton: false }, callback);
  },

  // This was simpler before I started trying everything I could think of
  onClickUploadFile() {
    if (!this.state.showUploadButton) {
      return;
    }
    this.disableUploadButton(function() {
      $.ajax({
        [...]
      });

    });
  },

  render() {
    var uploadButton;
    if (this.state.showUploadButton) {
      uploadButton = (
        <button onClick={this.onClickUploadFile}>Send</button>
      );
    }

    return (
      <div>
        {uploadButton}
      </div>
    );
  }

});

What I think happens is the state variable showUploadButton not being updated right away, which the React docs says is expected.

How could I enforce the button to get disabled or go away altogether the instant it's being clicked?

Appulus
  • 18,630
  • 11
  • 38
  • 46
Iulius Curt
  • 4,984
  • 4
  • 31
  • 55
  • Did you notice a problem or are you just wondering? Were you able to double submit? – cquezel Apr 04 '18 at 03:31
  • Formik library by default handles this issue, search about issubmitting in Formik website – Mohammad Oct 28 '19 at 12:25
  • Please look at @cquezel's answer for cleaner approach. Disabling form controls with ref (shown in the accepted answer) is an old thingy and might have been relevant in initial versions of ReactJs. – RBT Dec 03 '19 at 04:19
  • check this as reference https://sandny.com/2017/11/01/debounce-and-avoid-multiple-click-event-generation-on-react-js-components-lodash/ – ricky Jan 18 '22 at 17:07

15 Answers15

81

The solution is to check the state immediately upon entry to the handler. React guarantees that setState inside interactive events (such as click) is flushed at browser event boundary. Ref: https://github.com/facebook/react/issues/11171#issuecomment-357945371

// In constructor
this.state = {
    disabled : false
};


// Handler for on click
handleClick = (event) => {
    if (this.state.disabled) {
        return;
    }
    this.setState({disabled: true});
    // Send     
}

// In render
<button onClick={this.handleClick} disabled={this.state.disabled} ...>
    {this.state.disabled ? 'Sending...' : 'Send'}
<button>
cquezel
  • 3,859
  • 1
  • 30
  • 32
  • 4
    This is the cleanest approach and should be the accepted answer. – RBT Dec 02 '19 at 13:59
  • I also believe the same as @RBT, this is the most clean way to do it and we are doing it the same way also on projects. :) – Imran Rafiq Rather Jan 20 '20 at 10:48
  • @cquezel I get that each button will have its own handler, but `this.state.disabled` is the same for all buttons! isn't it? That's why it disabled all my buttons when I clicked one of them. I wanted to disable only that button which I clicked. – Zeeshan Ahmad Khalil May 07 '20 at 18:45
  • @cquezel your answer is perfect for a single button. – Zeeshan Ahmad Khalil May 07 '20 at 18:47
  • 1
    @ZeeshanAhmadKhalil "this.state" is different for each button. That's what the "this" is all about. "this" represents the state of each individual object. – cquezel May 08 '20 at 02:25
  • @cquezel now I get it, I'm doing it wrong from the start. I should put the above code in a separate class component and use it where I need it. I'll do that in the next project. Thanks for your precious time. – Zeeshan Ahmad Khalil May 08 '20 at 06:26
  • @cquezel why is the check in the event handler necessary? If `setState` is flushed before the browser event exits then the render should be complete as well? In that case the button would already be disabled, thus there is no way to trigger the click. – Adam Thompson May 27 '20 at 21:06
  • @AdamThompson The objective is to prevent the handler from being called more than once by disabling the button on the first click. The fact that state is flushed when the “handleClick” function enters guarantees that the “if” condition behaves correctly. Otherwise, the condition could “see” that the state is still enabled, not return, and do the processing a second time. – cquezel May 28 '20 at 22:22
  • @cquezel yeah I thought there was a guarantee the state would flush and re-render by the end of the first browser event, thus guaranteeing that the handler would never be executed because of the disabled state of the button - but although this currently seems to be true as per React implementation, there is no guarantee in the future. In this case, it is good to include the extra check even though it might work without it. – Adam Thompson May 29 '20 at 23:19
63

What you could do is make the button disabled after is clicked and leave it in the page (not clickable element).

To achieve this you have to add a ref to the button element

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

and then on the onClickUploadFile function disable the button

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

You can then style the disabled button accordingly to give some feedback to the user with

.btn:disabled{ /* styles go here */}

If needed make sure to reenable it with

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

Update: the preferred way of handling refs in React is with a function and not a string.

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


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

Update: Using react hooks

import {useRef} from 'react';
let btnRef = useRef();

const onBtnClick = e => {
  if(btnRef.current){
    btnRef.current.setAttribute("disabled", "disabled");
  }
}

<button ref={btnRef} onClick={onBtnClick}>Send</button>

here is a small example using the code you provided https://jsfiddle.net/69z2wepo/30824/

eltonkamami
  • 5,134
  • 1
  • 22
  • 30
  • 3
    This got me half way there, but the React team has deprecated giving ref a string value, and instead are using with it a callback: https://reactjs.org/docs/refs-and-the-dom.html – Martin Jan 29 '18 at 01:10
  • It giving me an error 'TypeError: self.btn.setAttribute is not a function' :( – Kushal Kumar Mar 30 '18 at 09:23
  • 1
    The best anser is [debounce](https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js) – Kushal Kumar Mar 30 '18 at 10:07
  • 4
    @KushalKumar How is debounce an adequate solution for this problem and what rate would be adequate for an only once scenario? – cquezel Apr 01 '18 at 15:46
  • The prefered way of handling refs in React has changed in 16.3. The React.createRef() function should be used. See: https://reactjs.org/docs/refs-and-the-dom.html – cquezel Apr 01 '18 at 16:19
  • @cquezel, It worked as I expected. If the user clicks a save button very fast, with the help of above debounce we can ignore the previous clicks. So it won't do the multiple calls. – Kushal Kumar Apr 03 '18 at 07:13
  • 8
    @KushalKumar My point is that this has nothing to do with speed. The requirement is "the button may be clicked only once". That is why I don't think debouce is the right tool for the job. – cquezel Dec 18 '18 at 13:06
  • I'm able to trigger the event as much as 5 times before it disables the button. – sigod Apr 17 '19 at 12:38
  • I appreciate the multiple updates over the past 4 years – Greg Micek Apr 28 '21 at 22:49
  • @KushalKumar debounce would not be right for this. A debounce would cause the button to be disabled and the AJAX request to be made AFTER x many seconds has passed, and swallow any clicks prior to that. For example, if you debounced this with a 500ms delay, nothing would happen for 500ms. Debounce isn't idea when limiting click methods because users want to see feedback instantly from their clicks. A throttle may be better than a debounce. – alexr89 Aug 05 '21 at 14:19
21

Tested as working one: http://codepen.io/zvona/pen/KVbVPQ

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

    this.state = {
      isButtonDisabled: false
    }
  }

  uploadFile() {
    // first set the isButtonDisabled to true
    this.setState({
      isButtonDisabled: true
    });
    // then do your thing
  }

  render() {
    return (
      <button
        type='submit'
        onClick={() => this.uploadFile()}
        disabled={this.state.isButtonDisabled}>
        Upload
      </button>
    )
  }
}

ReactDOM.render(<UploadArea />, document.body);
Samuli Hakoniemi
  • 18,740
  • 1
  • 61
  • 74
  • 15
    This would not solve the issue as state updations are debounced by React. Because of this, there would always be a delay in `this.state.isButtonDisabled` to get 'false' value. Clicking twice in quick succession would still register 2 onClick events. – Awol Aug 23 '17 at 09:52
  • @Awol makes a very good point, the batching of this.setState() causes double clicks to still happen. – ksloan Mar 15 '18 at 03:43
  • 1
    depending on the complexity of the component it should be fast enough for a double click and it's by far a better design than adding an attribute using refs. – Achim Koellner May 18 '18 at 12:23
  • 10
    @Awol React guarantees that setState inside interactive events (such as click) is flushed at browser event boundary. see my answer below. If you change read or set state in an event handler, this will not be a problem. – cquezel May 19 '18 at 18:19
  • @cquezel, I didn't know this. Learnt something new today. Good find, thanks! – Awol May 21 '18 at 12:28
  • Thanks for your answer, seem it works well, but be careful for asynchronous of `this.setState`. More safely, should do something in callback of `this.setState({}, () => { // do something });` – Vuong Tran Jan 29 '19 at 09:10
  • This, unfortunately, does not work. I was able to click a button 4 times in a row no problem with this approach. – C Bauer Apr 08 '19 at 12:54
9

You can try using React Hooks to set the Component State.

import React, { useState } from 'react';

const Button = () => {
  const [double, setDouble] = useState(false);
  return (
    <button
      disabled={double}
      onClick={() => {
        // doSomething();
        setDouble(true);
      }}
    />
  );
};

export default Button;

Make sure you are using ^16.7.0-alpha.x version or later of react and react-dom.

Hope this helps you!

saeta
  • 4,048
  • 2
  • 31
  • 48
  • you mean `useState` – lukas1994 Jul 18 '19 at 12:52
  • 2
    This button would then stay disabled forever, or until the page is refreshed or the component is rendered? This doesn't seem ideal? – alexr89 Aug 05 '21 at 14:27
  • 3
    This wouldn't immediately disable the button. You're relying on a setter to take effect, which requires a redraw of the component, so it won't be instantaneous. – gene b. Aug 09 '21 at 23:49
7

If you disable the button during onClick, you basically get this. A clean way of doing this would be:

import React, { useState } from 'react';
import Button from '@material-ui/core/Button';

export default function CalmButton(props) {
    const [executing, setExecuting] = useState(false);

    const {
        disabled,
        onClick,
        ...otherProps
    } = props;

    const onRealClick = async (event) => {
        setExecuting(true);
        try {
            await onClick();
        } finally {
            setExecuting(false);
        }
    };

    return (
        <Button
            onClick={onRealClick}
            disabled={executing || disabled}
            {...otherProps}
        />
    )
}

See it in action here: https://codesandbox.io/s/extended-button-that-disabled-itself-during-onclick-execution-mg6z8

We basically extend the Button component with the extra behaviour of being disabled during onClick execution. Steps to do this:

  1. Create local state to capture if we are executing
  2. Extract properties we tamper with (disabled, onClick)
  3. Extend onClick operation with setting the execution state
  4. Render the button with our overridden onClick, and extended disabled

NOTE: You should ensure that the original onClick operation is async aka it is returning a Promise.

Vajk Hermecz
  • 5,413
  • 2
  • 34
  • 25
  • This is a very clean approach. But one important thing: the async task duration need to be higher that 600/1000 ms !!! To be sure that it works all the time add 'await sleep(1000)' after 'await onClick();' . sleep is documented in the original example – Chris Dec 21 '20 at 09:28
  • Why is the 600/1000ms minimum? What happens if runtime is shorter? – Vajk Hermecz Dec 21 '20 at 16:31
  • if less than 600/1000ms then the someOperation() (in your example) run twice on double click. But this perfectly normal as the second click will be detected before. This can be easily reproduced if I change in your example 'await sleep(1000);' 'with await sleep(10);' – Chris Dec 23 '20 at 09:02
  • but again ClamButton is nice, I added it in my toolset :) – Chris Dec 23 '20 at 09:02
  • How does this prevent the button from being clicked only once? – cquezel Jun 05 '21 at 13:08
  • 1
    There's a problem with this approach: if the button unmounts while `await`ing the inner `onClick`, you end up modifying state of an unmounted component when you call `setExecuting(false);`, which is a React no-no. – Yarin Feb 03 '22 at 19:38
  • @Yarin do you have a recommendation or idea how to resolve that elegantly? Thx – Vajk Hermecz Feb 04 '22 at 15:13
  • @VajkHermecz Personally I simply wrapped the state change with an "only if mounted" condition. Now I looked around a bit and it turns out the React team decided this scenario is not actually a problem and they are going to remove the console error in the next version (or maybe it's already released?). Their discussion: https://github.com/facebook/react/pull/22114 – Yarin Feb 05 '22 at 11:06
6

If you want, just prevent to submit.

How about using lodash.js debounce

Grouping a sudden burst of events (like keystrokes) into a single one.

https://lodash.com/docs/4.17.11#debounce

<Button accessible={true}
    onPress={_.debounce(async () => {
                await this.props._selectUserTickets(this.props._accountId)
    }, 1000)}
></Button>
junho
  • 3,603
  • 3
  • 18
  • 25
2

By using event.target , you can disabled the clicked button. Use arrow function when you create and call the function onClick. Don't forget to pass the event in parameter.

See my codePen

Here is the code:

class Buttons extends React.Component{
  constructor(props){
    super(props)
    this.buttons = ['A','B','C','D']
  }

  disableOnclick = (e) =>{
    e.target.disabled = true
  }

  render(){
    return(

     <div>
        {this.buttons.map((btn,index) => (
          <button type='button' 
            key={index} 
            onClick={(e)=>this.disableOnclick(e)}
            >{btn}</button>
        ))}
      </div>
  )}

}
ReactDOM.render(<Buttons />, document.body);
dippas
  • 58,591
  • 15
  • 114
  • 126
Gazowski
  • 66
  • 4
1
const once = (f, g) => {
    let done = false;
    return (...args) => {
        if (!done) {
            done = true;
            f(...args);
        } else {
            g(...args);
        }
    };
};

const exampleMethod = () => console.log("exampleMethod executed for the first time");
const errorMethod = () => console.log("exampleMethod can be executed only once")

let onlyOnce = once(exampleMethod, errorMethod);
onlyOnce();
onlyOnce();

output

exampleMethod executed for the first time
exampleMethod can be executed only once
Ashok R
  • 19,892
  • 8
  • 68
  • 68
1

You can get the element reference in the onClick callback and setAttribute from there, eg:

      <Button
        onClick={(e) => {
          e.target.setAttribute("disabled", true);
          this.handler();
        }}            
      >
        Submit
      </Button>
rudresh solanki
  • 925
  • 9
  • 4
1

Keep it simple and inline:

<button type="submit"
        onClick={event => event.currentTarget.disabled = true}>
    save
</button>

But! This will also disable the button, when the form calidation failed! So you will not be able to re-submit.

In this case a setter is better.

This fix this set the disabled in the onSubmit of the form:


// state variable if the form is currently submitting
const [submitting, setSubmitting] = useState(false);

// ...
return (
<form onSubmit={e => {
                setSubmitting(true); // create a method to modify the element
            }}>

    <SubmitButton showLoading={submitting}>save</SubmitButton>
</form>
);

And the button would look like this:

import {ReactComponent as IconCog} from '../../img/icon/cog.svg';
import {useEffect, useRef} from "react";

export const SubmitButton = ({children, showLoading}) => {

    const submitButton = useRef();

    useEffect(() => {
        if (showLoading) {
            submitButton.current.disabled = true;
        } else {
            submitButton.current.removeAttribute("disabled");
        }
    }, [showLoading]);

    return (
        <button type="submit"
                ref={submitButton}>
            <main>
                <span>{children}</span>
            </main>
        </button>
    );

};
Hannes Schneidermayer
  • 4,729
  • 2
  • 28
  • 32
0

Another approach could be like so:

<button onClick={this.handleClick} disabled={isLoading ? "disabled" :""}>Send</button>
0

My approach is if event on processing do not execute anything.

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

this.state = {
  onProcess:false
   }
}

uploadFile() {
 if (!this.state.onProcess){
   this.setState({
     onProcess: true
   });
   // then do your thing
   this.setState({
     onProcess: false;
   });
 }    
}

render() {
  return (
    <button
      type='submit'
      onClick={() => this.uploadFile()}>
      Upload
    </button>
   )
  }
}

ReactDOM.render(<UploadArea />, document.body);
0

Try with this code:

class Form extends React.Component {
    constructor() {
        this.state = {
            disabled: false,
        };
    }

    handleClick() {
        this.setState({
            disabled: true,
        });

        if (this.state.disabled) {
            return;
        }

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

    render() {
        return (
            <button type="submit" onClick={() => this.handleClick()} disabled={this.state.disabled}>
                Submit
            </button>
        );
    }
}

ReactDOM.render(<Form />, document.getElementById('root'));
Kourosh Neyestani
  • 781
  • 2
  • 9
  • 16
0

If still someone facing with such issue, the solution is much easier then creating custom components etc...

Just,

  1. set a state value called disabled by default false.
  2. assign it to your button's disabled property.
  3. in onPress firstly set it to true, then do your stuff....
  4. then set it to false on your component's un mount...
function MyClickable() {
    const [disabled,setDisabled] = useState(false)
useEffect(() => {
return () => setDisabled(false)
},[])

    const onPress  = useCallback(() => {
                         setDisabled(true);
                         // do your stuff
                    },[]);

     <TouchableOpacity disabled={disabled} onPress={onPress}>
          // your things
     </TouchableOpacity>
Ozzie
  • 475
  • 1
  • 5
  • 20
0

Try with this code

<button 
    onClick={async (e) => {
    e.currentTarget.disabled = true;
    await onClickUploadFile();
    e.currentTarget.disabled = false;
}}>
    Upload
</button>
Vladimir Salguero
  • 5,609
  • 3
  • 42
  • 47