64

I create a debounced version of a function with underscore:

var debouncedThing = _.debounce(thing, 1000);

Once debouncedThing is called...

debouncedThing();

...is there any way to cancel it, during the wait period before it actually executes?

user1031947
  • 6,294
  • 16
  • 55
  • 88
  • possible duplicate of [Using debounce function in underscore](http://stackoverflow.com/questions/7026000/using-debounce-function-in-underscore) – Yury Tarabanko Mar 12 '15 at 14:56
  • 2
    That question is unclear. Mine is specific. That question does not provide an answer to my question. – user1031947 Mar 12 '15 at 14:57
  • Sure you can implement your own version of debouncer that will somehow expose `timeout` or a function clearing the timeout. – Yury Tarabanko Mar 12 '15 at 14:59

5 Answers5

113

If you use the last version of lodash you can simply do:

// create debounce
const debouncedThing = _.debounce(thing, 1000);

// execute debounce, it will wait one second before executing thing
debouncedThing();

// will cancel the execution of thing if executed before 1 second
debouncedThing.cancel()

Another solution is with a flag:

// create the flag
let executeThing = true;

const thing = () => {
   // use flag to allow execution cancelling
   if (!executeThing) return false;
   ...
};

// create debounce
const debouncedThing = _.debounce(thing, 1000);

// execute debounce, it will wait one second before executing thing
debouncedThing();

// it will prevent to execute thing content
executeThing = false;
Carlos Ruana
  • 2,138
  • 1
  • 15
  • 14
5

The docs (I'm looking at 1.9.1 right now) say that you should be able to do:

var fn = () => { console.log('run'); };
var db = _.debounce(fn, 1000);
db();
db.cancel();
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.6/underscore-umd-min.js"></script>

This will do the thing that the OP wants to do (and what I wanted to do). It will not print the console message.

Julian
  • 4,176
  • 19
  • 40
Stephan Samuel
  • 648
  • 6
  • 12
3

For anyone else using React with state hooks. Wrap the debounce event in a ref to then access elsewhere:

const [textInputValue, setTextInputValue] = React.useState<string>('')
    
const debouncedSearch = React.useRef(
    debounce((textInputValue) => {
        performSearch(textInputValue)
    }, 300),
).current

React.useEffect(() => {
    // cancel any previous debounce action (so that a slower - but later - request doesn't overtake a newer but faster request)
    debouncedSearch.cancel()
    if (textInputValue !== '') {
        debouncedSearch(textInputValue)
    }
}, [textInputValue])
S..
  • 5,511
  • 2
  • 36
  • 43
2

the Vanilla js variant with cancellable wrap

Note that this solution doesn't require you to modify an external debounce function or to even use an external one. The logic is done in a wrapepr function. Debounce code provided.


The easiest way to allow to cancel an already called function within its debounce period is to call it from a cancelable wrap. Really just add 3 lines of code and an optional condition.

const doTheThingAfterADelayCancellable = debounce((filter, abort) => {
  if (abort) return

  // here goes your code...
  // or call the original function here

}, /*debounce delay*/500)


function onFilterChange(filter) {
  let abort = false

  if (filter.length < 3) { // your abort condition
    abort = true
  }

  // doTheThingAfterADelay(filter) // before
  doTheThingAfterADelayCancellable(filter, abort) // new wrapped debounced call
}

You cancel it by calling it again with abort = true.

The way it works is that it clears the previous timeout fn and sets a new one as it always does, but now with if (true) return path.

You can also do it manually from another code...

doTheThingAfterADelayCancellable(null, true)

...or wrap it and call with cancelBounce()

function cancelBounce() {
  doTheThingAfterADelayCancellable(null, true)
}

For reference, this is your classic debounce function taken from Underscore. It remains intact in my example.

// taken from Underscore.js
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
export function debounce(func, wait, immediate) {
  let timeout
  return function() {
    let context = this, args = arguments
    let later = function() {
      timeout = null
      if (!immediate) func.apply(context, args)
    }
    let callNow = immediate && !timeout
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
    if (callNow) func.apply(context, args)
  }
}
Qwerty
  • 29,062
  • 22
  • 108
  • 136
1

What I've done is used _.mixin to create a _.cancellableDebounce method. It's nearly identical to the original except for two new lines.

_.mixin({
    cancellableDebounce: function(func, wait, immediate) {
        var timeout, args, context, timestamp, result;

        var later = function() {
          var last = _.now() - timestamp;

          if (last < wait && last >= 0) {
            timeout = setTimeout(later, wait - last);
          } else {
            timeout = null;
            if (!immediate) {
              result = func.apply(context, args);
              if (!timeout) context = args = null;
            }
          }
        };

        return function() {
          context = this;
          args = arguments;
          timestamp = _.now();
          var callNow = immediate && !timeout;
          if (!timeout) timeout = setTimeout(later, wait);
          if (callNow) {
            result = func.apply(context, args);
            context = args = null;
          }

          // Return timeout so debounced function can be cancelled
          result = result || {};
          result.timeout = timeout;

          return result;
        };
    }
});

USAGE:

var thing = function() {
    console.log("hello world");
}

var debouncedThing = _.cancellableDebounce(thing, 1000);
var timeout = debouncedThing().timeout;

clearTimeout(timeout);
Dexygen
  • 12,287
  • 13
  • 80
  • 147