Home > Software design >  Make an onClick function wait the previous call to end
Make an onClick function wait the previous call to end

Time:03-31

I'm working with React, And I have a problem synchronizing clicks successively (onClick events):

My button triggers a function that calls two functions resetViews and chargeViews (chargeViews takes time), so when I click two times rapidely:

  • resetView is executed fine (it sets viewsRef.current to null)
  • then chargeViews is called but as it is taking time it won't update viewsRef.current right after.
  • so resetView is called but viewsRef.current is still null (because chargeViews doesn't update it yet) so it will do nothing,
  • then I get two delayed executions of chargeViews
  • so after two clicks I got 1 execution of resetView and two delayed executions of chargeViews

What I want is after clicking, block everything (even if the user clicks we do nothing) and execute resetView then chargeViews and then unblock to receive another click and do the same work.

<MyButton onClick={() => {
  resetViews();
  chargeViews();
 }}> 
  Click me 
 </MyButton>

The function that takes time

const chargeViews = () => {
  if(!viewsRef.current){
   ...
   reader.setUrl(url).then(()=>{
   reader.loadData().then(()=>{
   ...
   viewsRef.current = reader.getData();
})})
}}

The function that getting ignored if I click so fast, (It works fine if I click and I wait a little bit then I click again) but if I click and click again fast It is ignored.

const resetViews = () => {
  if (viewsRef.current){
   ...
   viewsRef.current = null;
}}

CodePudding user response:

I'm not totally sure to grasp the whole issue... this would be a comment if it didn't require such a long text.

Anyway as far as you need to disable the button once it's clicked you should deal with it at the beginning of its onclick handler:

$(this.event.target).prop('disabled', true);

and reset it at the end:

$(this.event.target).prop('disabled', false);

In general what you did was correct in terms of calling a number of functions to be executed in chain inside the handler. But those 2 functions seem to have a promise invocation.. in that case those won't be executed in chain waiting for the first one to finish.

So you should pass the second invocation as a callback to the first, to have the chance to call it ONLY once the first one finished its job.

I hope someone will just go straight to the point suggesting how making an async function "await" so that whatever it does, when invocated it will be waited for to complete before the next statement is evaluated. Usually it's just a matter of adding await before its signature but there are some caveats.

CodePudding user response:

First, you need to convert your Promise-utilizing functions into async functions and then await them when invoking them. This will make it easier to control the order of execution:

const chargeViews = async () => {
  if(!viewsRef.current){
   ...
   await reader.setUrl(url);
   await reader.loadData();
   ...
   viewsRef.current = reader.getData();
  }
}

Then, you need an isExecuting ref that will be true when other invokations are executing and false when none are currently executing:

const isExecuting = useRef(false);

const handleClick = async () => {
  if (!isExecuting.current) {
    // block other clicks from performing actions in parallel
    isExecuting.current = true;
    try {
      resetViews();
      await chargeViews();
    } finally {
      // unblock other clicks
      isExecuting.current = false;
    }
  }
};

Lastly, use the newly-created handleClick function in your JSX:

<MyButton onClick={handleClick}>
  Click me
</MyButton>
  • Related