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 setsviewsRef.current
to null)- then
chargeViews
is called but as it is taking time it won't updateviewsRef.current
right after. - so
resetView
is called butviewsRef.current
is still null (becausechargeViews
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 ofchargeViews
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>