I have some ionic segment buttons that essentially act as toggle switches. I'm trying to limit the rate at which the effects of the toggles can be switched/selected) and have tried Debounce & Throttle from both lodash and now underscores without success.
I'm currently using underscores and am still having the issue that the function is still being called freely despite the use of debounce.
Here's my code:
const SensorMeasurements: React.FC = () => {
//.....
const setOptionsHandler = (value: string, option: string, key: string) => {
if (value === "true") {
console.log("RESULT: True / " value " / " option " / " key);
if (!settingsHandler(key)) {
setControlHandler(option, false); //toggle on
console.log("TOGGLE " option " ON!");
}
}
else { // false
console.log("RESULT: False / " value " / " option " / " key);
if (settingsHandler(key)) {
setControlHandler(option, false); //toggle off
console.log("TOGGLE " option " OFF!");
}
}
}
const setOptionsHandlerThrottled = _.debounce(setOptionsHandler, 5000, true);
const setControlHandler = async (input: string, notify?: boolean) => {
let sysCommand = "";
switch (input) {
case "Reset":
sysCommand = "R";
break;
case "Test":
sysCommand = "T";
break;
case "Calibration":
sysCommand = "B";
break;
case "Logging":
sysCommand = "L";
break;
case "Trigger":
sysCommand = "H";
break;
case "Measuring":
sysCommand = "M";
break;
case "Unit":
sysCommand = "U";
break;
}
try {
const abControl = str2ab(sysCommand);
const dvControl = new DataView(abControl.buffer);
//console.log("CONTROL BUFFER", abControl);
await BleClient.initialize();
//if (isAndroid && (devices.deviceId ?? true)) {
// await BleClient.getDevices(devices.deviceId);
// await BleClient.disconnect(devices.deviceId);
//}
await BleClient.getDevices(devices.deviceId);
await BleClient.connect(devices.deviceId, (deviceId) => onDisconnect(deviceId));
await BleClient.write(devices.deviceId, SERV_SYSTEM_CONTROL, CHAR_OPERATIONAL_MODE, dvControl);
if (notify !== false) {
present(input ' Command Sent.', 3000);
}
} catch (error) {
CatchError(error, "Disconnected");
}
}
const settingsHandler = (string: string) => {
return trueOrFalse(iTrueStates, string) as unknown as string;
}
const trueOrFalse = (array: string[], string: string) => {
//check if string is in the array - returns boolean
return array.includes(string);
}
return (
<IonSegment onIonChange={e => setOptionsHandlerThrottled(e.detail.value as string, "Trigger", "statusSensorHighTrigger")} className="lowercase" color="brand" value={settingsHandler("statusSensorHighTrigger")}>
<IonSegmentButton value="false">
<IonLabel>Low</IonLabel>
</IonSegmentButton>
<IonSegmentButton value="true">
<IonLabel>High</IonLabel>
</IonSegmentButton>
</IonSegment>
);
export default SensorMeasurements;
Why is setOptionsHandler
still being called when the IonSegmentButton changes regardless of the debounce timeout? Am I accidentally creating a new debounced function every time it's called?
CodePudding user response:
As you guessed, the problem is that you are creating a new debounced function every time the component is rendered. The multiple debounced functions are unaware of each other's timeouts. If we omit the other code for clarity, the problem is relatively easy to see:
// This function is called on every render
const SensorMeasurements: React.FC = () => {
//.....
const setOptionsHandler = // ...
// This constant is recreated on every invocation of SensorMeasurements
const setOptionsHandlerThrottled = _.debounce(setOptionsHandler, 5000, true);
return (
// The onIonChange event handler is unique on each render
<IonSegment onIonChange={e => setOptionsHandlerThrottled(e.detail.value as string, "Trigger", "statusSensorHighTrigger")} className="lowercase" color="brand" value={settingsHandler("statusSensorHighTrigger")}>
<IonSegmentButton value="false">
<IonLabel>Low</IonLabel>
</IonSegmentButton>
<IonSegmentButton value="true">
<IonLabel>High</IonLabel>
</IonSegmentButton>
</IonSegment>
);
}
The simple solution is to use a class component instead. By creating the debounced function in the constructor, it can be retained between renderings. Most other functions can be free-standing at module scope, since they do not depend on component state.
Do note that if the instance of the component is replaced as a whole (for example because a containing component is re-rendered), the timeout will still be reset. In that case, you might need to lift the debounced function into the larger component.
// Module scope, not inside component
const setOptionsHandler = (value: string, option: string, key: string) => {
// Same as before
}
const setControlHandler = async (input: string, notify?: boolean) => {
// Same as before
}
const settingsHandler = (string: string) => {
// Same as before
}
const trueOrFalse = (array: string[], string: string) => {
// Same as before
}
// Class component
class SensorMeasurements extends React.Component {
setOptionsHandlerThrottled: typeof setOptionsHandler;
constructor() {
// This debounced function persists throughout the lifetime of the component
this.setOptionsHandlerThrottled = _.debounce(setOptionsHandler, 5000, true);
}
render() {
return (
<IonSegment onIonChange={e => this.setOptionsHandlerThrottled(e.detail.value as string, "Trigger", "statusSensorHighTrigger")} className="lowercase" color="brand" value={settingsHandler("statusSensorHighTrigger")}>
<IonSegmentButton value="false">
<IonLabel>Low</IonLabel>
</IonSegmentButton>
<IonSegmentButton value="true">
<IonLabel>High</IonLabel>
</IonSegmentButton>
</IonSegment>
);
}
}
export default SensorMeasurements;
A possible alternative to using _.debounce
would be to use the debounce
operator from RxJS. In that case, a similar consideration would apply that you might need to lift the subject to a larger containing component.
In the comments, you stated that the problem went away when you removed the async
/await
syntax from the setControlHandler
function. I cannot explain this; with your original code, you will create a new debounced function on each render regardless. I suspect some other, unrelated bug is preventing the event handler from triggering again in that case.
As an encore, I would like to show you a cleaner way to write your setControlHandler
function:
// The mapping from inputs to sysCommands is entirely static, so you can
// define it in advance.
const inputCommand = {
Reset: 'R',
Test: 'T',
Calibration: 'B',
Logging: 'L',
Trigger: 'H',
Measuring: 'M',
Unit: 'U',
};
const setControlHandler = async (input: string, notify?: boolean) => {
// Replacing your switch pyramid by a property lookup
const sysCommand = inputCommand[input];
// Remainder as before
}
CodePudding user response:
Ultimately I ended up using AwesomeDebouncePromise as this worked best. In order to prevent the creation of a new Debounced function every rerender I wrapped the debounced function in a useMemo.
const setOptionsHandlerThrottled = useMemo( () => AwesomeDebouncePromise(setOptionsHandler, 3000), [iTrueStates] );