8

I need a simple debounce function with immediate always true.
Without resorting to lodash and with the help of Can someone explain the "debounce" function in Javascript , I implemented it as following,

function debounce(func, wait) {
    var timeout;
    return function() {
        if (!timeout) func.apply(this, arguments);
        clearTimeout(timeout);
        timeout = setTimeout(()=>{timeout = null}, wait);
    };
};

It works as expected until I need to debounce axios request. Assumed I have a debounced axios method, I would like the calling method to be as usual, which means my debounced axios method should return promise I believe.

   //the calling method should not change   
   debounced_axios().then(res => {...}).catch(err => {...}) 

The essence of original debounce implementation is to just run func once in a wait timeframe, but how do I just return one promise in a wait timeframe ?

Then I came up with the following solution

all_timers = {}
function debounce_axios(input, wait) {
    return new Promise((resolve, reject) => {
        let timer = all_timers.[input] //check if it is a repeated request, pseudo code
        if (!timer) {
            axios(input).then(res=>{
                resolve(res)
            }).catch(err => {
                reject(err)
            })
        }
        clearTimeout(timer);
        timer = setTimeout(()=>{timer = null}, wait);
        all_timers[input] = timer
    };
};

So the essence of my debounce_axios is to let the promise stay in pending state for the repeated request.Then the calling method debounced_axios().then(res => {...}).catch(err => {...}) does not need to change.

The answer here Are JavaScript forever-pending promises bad? said "There should be no side effect."

But I am still not 100% sure about letting a promise stay in pending forever.

The other question is that Promise Anti patterns suggested not creating unnecessary promise. But in my case creating a new promise seems necessary.

In a nutshell is there a simple to way to debounce axios request (or any request returns promise) ?

Qiulang
  • 10,295
  • 11
  • 80
  • 129
  • What exactly does "*`.then(res => {...}).catch(err => {...})`*" do? Why wouldn't you simply put it inside of the function you used to create `debounced_axios`? – Bergi Apr 30 '19 at 12:07
  • various even handlers to manipulate DOM so I can't input them inside debounced_axios – Qiulang Apr 30 '19 at 12:27
  • Well maybe you shouldn't call it `debounced_axious` any more, but why not completely debounce the whole thing? – Bergi Apr 30 '19 at 12:56
  • Yes I agree debounce the ui may be a better idea. But the fact it is a SPA app with quite big code bases, too many places need to be modified while we did wrap the axios methods in one util class, so it is easy to just modify it. BTW I got this idea from you. Thanks – Qiulang Apr 30 '19 at 13:26
  • 1
    If you don't like for-ever-pending promises, would it be acceptable to immediately reject a request that is made too soon? – trincot Apr 30 '19 at 13:36
  • @trincot I think that can be another option. Unfortunately in my case I am dealing with an old code base and if I use that, many places need to be modify to, i.e. in the catch block adding code to handle the debounce reject. That's another reason I come up with forever pending promise, so I can touch the old code as little as possible – Qiulang May 01 '19 at 14:25

2 Answers2

4

But I am still not 100% sure about letting a promise stay in pending forever.

I agree that it's not a good idea. A better approach would be to move the entire promise chain inside the debounced function.

Another option would be to return a cached value when the debounced call does not trigger a new request. This would solve your problem that you always need to return a promise:

function debounce(func, wait) {
    var timeout, value;
    return function() {
        if (!timeout) value = func.apply(this, arguments);
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            timeout = value = null;
        }, wait);
        return value;
    };
}

Of course that would mean that in some cases, multiple then handlers will be called when your request finishes. It depends on your application whether that is a problem or just superfluous work.

The other question is that Promise Anti patterns suggested not creating unnecessary promise. But in my case creating a new promise seems necessary.

Only one promise is necessary: when you create the never-resolved one. You can write that as

function debounce(func, wait) {
    var timeout;
    const never = new Promise(resolve => {/* do nothing*/});
    return function() {
        const result = timeout ? never : func.apply(this, arguments);
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            timeout = null;
        }, wait);
        return result;
    };
}

Or at least avoid the .then(resolve).catch(reject) part. Better write

function debounce(func, wait) {
    var timeout;
    return function() {
        return new Promise(resolve => {
            if (!timeout) resolve(func.apply(this, arguments));
//                        ^^^^^^^
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                timeout = null;
            }, wait);
        });
    };
}

And should you consider to reject the promise in case that the timeout has not yet occurred (so that the calling code can handle the rejection), you don't need new Promise either:

function debounce(func, wait) {
    var timeout;
    return function() {
        const result = timeout
          ? Promise.reject(new Error("called during debounce period"))
          : Promise.resolve(func.apply(this, arguments));
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            timeout = null;
        }, wait);
        return result;
    };
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Hi I am verifying each of your suggestion. For returning the cache value, I am not sure if it will work. Because I assume the cached promise will be called more than once in the calling method 's promise chain, right ? – Qiulang May 01 '19 at 14:15
  • I never cached promise before so I guess I was asking a newbie question. So it is safe to cache promise and reuse it ? Is it safe to run the promise chain with cached promise ? – Qiulang May 01 '19 at 14:44
  • Yes, [caching promises](https://stackoverflow.com/a/18745499/1048572) is totally normal and works well. A promise is not "called", it is a result value - the request will be made only once, but the response will be passed into as many handlers as were registered with `.then()`. – Bergi May 01 '19 at 16:31
  • Hi I am still verifying your answer :$. For "Or at least avoid the .then(resolve).catch(reject) part." I think you return a pending forever promise for debounced call, just like the one creating the never-resolved promise. And we back to square one, i.e. my original question that is it good to do that? Isn't that the case ? – Qiulang May 04 '19 at 08:18
  • 1
    @Qiulang Yes, I think there are at least 3 better approaches, but if you still want to do it then at least do it properly (as shown in the second or third snippet). – Bergi May 04 '19 at 09:21
1

In essence, you need to share result of your debounce function. In your case, thats a promise:

const debouncedGetData = debounce(getData, 500)
let promiseCount = 0
let resultCount = 0
test()

function test() {
  console.log('start')
  callDebouncedThreeTimes()
  setTimeout(callDebouncedThreeTimes, 200)
  setTimeout(callDebouncedThreeTimes, 900)
}

function callDebouncedThreeTimes () {
   for (let i=0; i<3; i++) {
      debouncedGetData().then(r => {
        console.log('Result count:', ++resultCount)
        console.log('r', r)
      })
   }
}

function debounce(func, wait) {
    let waiting;
    let sharedResult;
    return function() {
        // first call will create the promise|value here
        if (!waiting) {
          setTimeout(clearWait, wait)
          waiting = true
          sharedResult = func.apply(this, arguments);
        }
        // else new calls within waitTime will be discarded but shared the result from first call

        function clearWait() {
          waiting = null
          sharedResult = null
        }

        return sharedResult
    };
}

function getData () {
  console.log('Promise count:', ++promiseCount)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       resolve(666)
    }, 1000)
  })
}
Doğancan Arabacı
  • 3,934
  • 2
  • 17
  • 25
  • 1
    I believe that is not correct b/c (a) the normal debounce method should return debounced method (b) in axios case, when the debounced situation returns nothing return, the calling method will get UnhandledPromiseRejectionWarning: TypeError: (...).then is not a function. – Qiulang Apr 30 '19 at 13:22
  • I believe debounce function here can be used for any kind of function since it's not dependent on promises – Doğancan Arabacı Apr 30 '19 at 13:43
  • Thanks I will verify that and get back to you. – Qiulang May 01 '19 at 14:16