0

There is a search input with handler using ordinary debounce function (Vue component):

created() {
      this.handleChange = debounce(async (ev) => {
        this.loading = true;
        const { target: { value: search } } = ev;
        if (search) {
          const response = await searchRepos({ search });
          const repos = await response.json(response);
          console.log(search);
          console.log(repos?.items);
          this.repos = repos?.items;
        } else {
          this.repos = [];
        }
        this.loading = false;
      }, 500);
    }

debounce

const debounce = (callback, wait) => {
  let timeoutId = null;
  return (...args) => {
    window.clearTimeout(timeoutId);
    timeoutId = window.setTimeout(() => {
      callback.apply(null, args);
    }, wait);
  };
}

Works fine, but here is a moment. Callbacks are not waiting each other and it might create a situation, when previous callback return value after the next callback.

For example, sending a request when user erase penult symbol, during this user erase last and its callback ending faster, because of no request. Then previous callback return value, and non valid info shows up. Even tough we send request, there is no guarantee we receive response after previous.

My solution is in improving debounce like this:

export const debounce = (callback, waitTime) => {
  let timeoutId = null;
  let isPreviousPerforming = false;
  let callbacksOrder = [];
  const performRest = async () => {
    for (const order of callbacksOrder) {
      await callback.apply(null, [order.ev]);
    }
    callbacksOrder = []
  };
  return (ev) => {
    window.clearTimeout(timeoutId);
    timeoutId = window.setTimeout(async () => {
      if (isPreviousPerforming) {
        callbacksOrder.push({ ev });
        return;
      }
      isPreviousPerforming = true;
      await callback.apply(null, [ev]);
      await performRest();
      isPreviousPerforming = false;
    }, waitTime);
  };
}

But here is a moment again: small probability that between for and callbacksOrder = [] we could receive an event that will be pushed to callbacksOrder and we will delete it. Or do I misunderstand something?

QUESTION: What are possible solution or best practice to handle search input this way?

P.S. v-debounce works similar ordinary debounce.

Callbacks should be solved successively.

Var Lynx
  • 1
  • 2

1 Answers1

0

I wouldn't await all the request if you don't need the result anyway. Instead, you could abort running requests with AbortController when starting a new request, which rejects all current awaits. But you would have to build this into your searchRepos().

Something like this should work:

const debounceWithAbort = (callback, wait) => {
  let timeoutId = null;
  let controller = null; // <--- add controller reference
  return (...args) => {
    window.clearTimeout(timeoutId);
    if (controller) { // <--- abort running fetches
      controller.abort()
    }
    timeoutId = window.setTimeout(() => {
      controller = new AbortController // <--- create controller
      callback.apply(null, [controller.signal, ...args]); // <--- pass in signal
    }, wait);
  };
}

then you can pass the signal to searchRepos():

      this.handleChange = debounceWithAbort(async (signal, ev) => { // <--- take signal
        const { target: { value: search } } = ev;
        if (!search) {
          this.repos = [];
          return
        }
        this.loading = true;
        try{
          const response = await searchRepos({ search }, signal); // <--- pass signal to request and use it there
          const repos = await response.json(response);
        } catch (e) {
          return // <--- aborted
        }
        this.repos = repos?.items;
        this.loading = false;
      }, 500);
    }

What you do with signal inside searchRepos() depends on how you send the request, but I think by now AbortController is supported everywhere.

Moritz Ringler
  • 9,772
  • 9
  • 21
  • 34