Home > Software engineering >  Underscore's debounce seemingly not taking effect with function calling an async function
Underscore's debounce seemingly not taking effect with function calling an async function

Time:07-02

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] );
  • Related