What's the best way to prevent any further function call after the first click, despite the asynchronous operation of setState() which complicates the task?
The mechanism does not necessarily have to be based on React states, but must nevertheless be easily reversible (e.g. by login() in case of wrong credentials).
In the examples below, refs are only used to simulate very fast clicks.
The solution to check the state immediately upon entry to the handler does not seem to be working: https://stackoverflow.com/a/49642037 (https://codesandbox.io/s/stupefied-moon-uouz2)
The solution to directly disabling button via a ref is not convenient at all in the case of a form that can be submitted from any field by pressing enter: https://stackoverflow.com/a/35316216
A solution that seems to work with Class components, but looks a bit ugly and which strangely causes a double setState under codesandbox.io (why?) regardless of the number of clicks, but not locally (https://codesandbox.io/s/ecstatic-river-wuhov):
import React, { Component } from "react";
export default class Login extends Component {
constructor() {
super();
this.state = { disabled: false };
this.ref = React.createRef();
}
componentDidMount() {
console.log("click");
this.ref.current.click();
console.log("click");
this.ref.current.click();
console.log("click");
this.ref.current.click();
}
onClick = () => {
this.setState(({ disabled }) => {
disabled || this.login();
return { disabled: true };
});
};
login = () => console.log("login");
render() {
return (
<button
ref={this.ref}
onClick={this.onClick}
disabled={this.state.disabled}
>
Login
</button>
);
}
}
A slightly heavier variant but without that strange side effect (https://codesandbox.io/s/compassionate-bouman-hjtv6):
import React, { Component } from "react";
export default class Login extends Component {
constructor() {
super();
this.state = { disabled: false };
this.ref = React.createRef();
}
componentDidMount() {
console.log("click");
this.ref.current.click();
console.log("click");
this.ref.current.click();
console.log("click");
this.ref.current.click();
}
componentDidUpdate(prevProps, prevState) {
const { disabled } = this.state;
if (prevState.disabled !== disabled && disabled) {
this.login();
}
}
onClick = () => this.setState({ disabled: true });
login = () => console.log("login");
render() {
return (
<button
ref={this.ref}
onClick={this.onClick}
disabled={this.state.disabled}
>
Login
</button>
);
}
}
For functional components, this solution works but causes an eslint warning (React Hook useEffect has a missing dependency) in development mode (is this bad?) (https://codesandbox.io/s/frosty-allen-tgs42):
import React, { useRef, useEffect, useState } from "react";
export default () => {
const [disabled, setDisabled] = useState(false);
const ref = useRef();
useEffect(() => {
console.log("click");
ref.current.click();
console.log("click");
ref.current.click();
console.log("click");
ref.current.click();
}, []);
useEffect(() => {
disabled && login();
}, [disabled]);
const onClick = () => setDisabled(true);
const login = () => console.log("login");
return (
<button ref={ref} onClick={onClick} disabled={disabled}>
Login
</button>
);
};
The solution for class component that caused a double setSate under codesandbox.io seems to work without problem for functional components (pattern that I'm currently using in production)(https://codesandbox.io/s/kind-hoover-ls3t3):
import React, { useRef, useEffect, useState } from "react";
export default () => {
const [disabled, setDisabled] = useState(false);
const ref = useRef();
useEffect(() => {
console.log("click");
ref.current.click();
console.log("click");
ref.current.click();
console.log("click");
ref.current.click();
}, []);
const onClick = () =>
setDisabled(prevDisabled => {
prevDisabled || login();
return true;
});
const login = () => console.log("login");
return (
<button ref={ref} onClick={onClick} disabled={disabled}>
Login
</button>
);
};
Since none of these solutions seem really satisfactory, any suggestions?