jayphelps/core-decorators

@throttle / @debounce - allow to `cancel` method call

Closed this issue ยท 8 comments

It would be great to add functionality to cancel method execution

Currently I use @decorate on lodash.throttle and lodash.debounce to make it work

class EventHelper() {
    cancelChange() {
        this.triggerChange.cancel();
    }

    @decorate(_.debounce, 100)
    triggerChange() {
         // do something
    }
}

That's work just fine, but if i want to use @autobind on it, .bind removes .cancel() function from method.

So I had looked through @throttle and @debounce code and noticed that timeout ids are stored in class instance meta

So the fastest and dumbest solution will be:

debounce.cancel = function(instance, method) {
    const { debounceTimeoutIds } = metaFor(instance);

    let methodKey;

    for (let key in instance) {
        if (instance[key] === method) {
            methodKey = key;
            break;
        }
    }

    clearTimeout(debounceTimeoutIds[methodKey]);

    delete debounceTimeoutIds[methodKey];
}

and

class EventHelper() {
    cancelChange() {
        debounce.cancel(this, this.triggerChange);
    }

    @autobind
    @debounce(100)
    triggerChange() {
         // do something
    }
}

But that syntax can make people cry and worth discussion.

May be put it to utils as debounceCancel

Or make method argument as optional and clear all timeouts in meta

Or create another method in class that will cancel its debounce like triggerChange => __cancelTriggerChange (warn or throw exception if method exists)

Definitely down to discuss this, in the meantime just in case you hadn't thought of it you can work around this:

class EventHelper() {
  cancelChange() {
    this.triggerChangeDebounced.cancel();
  }

  @autobind
  triggerChange() {
    this.triggerChangeDebounced(this);
  }

  @decorate(_.debounce, 100)
  triggerChangeDebounced(context) {
    // do something with context, like context.kickDog()
  }

  kickDog() {}
}

Let me think through some syntax options for cancel support.

@jayphelps thanks for your response,

I looked up through my code and noticed that I need to .cancel delayed execution only when i destroying classes (in other cases it can be done without using @decorate)

So if you want to keep it simple and easy for people to use i think you can adopt clearTimeout function syntax:

import {debounce, clearTimeouts} from 'core-decorators';

class Bar {
    @debounce(1000)
    baz() {
          // do something
    }
}

const foo = {
    bar:  new Bar(),
    bas: new Bar()
};

foo.bar.baz();
foo.bas.baz();

delete foo.bar;
delete foo.bas;

clearTimeouts(foo.bar);

Here foo.bar.baz - will be canceled, foo.bas.baz will be executed.

clearTimeouts - will iterate through instance meta and clear every meta timeout id

I think it's will be easiest way to implement and use

+1 Also wanted to cancel() debounce after React component unmount (isMounted() is deprecated!). Cant use clearTimeout, because youre hiding your metas behind non-exported Symbol.

Is a cancel() function on the debounced method ideal? I'm happy with that.

class Example extends Component {
  @debounce(1000)
  doStuff() {
    // do stuff
  }

  componentWillUnmount() {
    this.doStuff.cancel();
  }
}

@nehaleem also--in the meantime if you're also already using lodash, you can accomplish this with the @decorate decorator and lodash's _.debounce:

class Example extends Component {
  @decorate(_.debounce, 1000)
  doStuff() {
    // do stuff
  }

  componentWillUnmount() {
    this.doStuff.cancel();
  }
}

If you need autobinding too, you can do this:

class Example extends Component {
  @autobind
  doStuff() {
    this.doStuffDebounced();
  }

  @decorate(_.debounce, 1000)
  doStuffDebounced() {
    // do stuff
  }

  componentWillUnmount() {
    this.doStuffDebounced.cancel();
  }
}

@jayphelps Yeah, thats what i used for now, with future "//TODO" ๐Ÿ˜„

This turns out to be a non-trivial thing to do.

The issue is around making it so that the cancel function is unique for each instance of that class so it does not cancel all debounces for every instance.

This is a similar problem that @decorate had and I sort of solve by generating a new function for each instance, the first time you look up that property: https://github.com/jayphelps/core-decorators.js/blob/master/src/decorate.js

I may try to do that here, but just a heads up that it probably will take some time..

There are no "elegant" ways to do it with decorators now, so i end up with:

class EventHelper() {
  cancelChange() {
    this.triggerChange.cancel()
  }

  triggerChange = _.debounce(() => {
    // do something
  }, 100)
}

Anyways, thanks you @jayphelps ๐Ÿ‘