Search code examples
javascriptinputtimeoutfetch-apithrottling

Execute a function not more than once every 500 milliseconds while user types continuously


I'am trying to implement something like autocomplete, so I'am running the function when oninput event fires. Because I'am making a fetch request instead of running it on every change I'd like to run it not more than once in (maybe) 500ms. Is there a way to do this?

<body>
   <input id="input" type="text">

   <script>
      function filterData(substr) {
         fetch(url)
           .then(response => response.json())
           .then(data => {
              let filteredData = data.filter(person => person.name.includes(substr));
              print(filteredData);
            })
       }
       document.getElementById("input").oninput = (e) => filterData(e.target.value);
   </script>
</body>

Solution

  • The OP's description resembles a throttled behavior. There are various libraries/implementations available in order to achieve both the throttled process and its sibling the debounced process. Commonly used are the methods of lodash and/or underscore.js.

    The next provided example code uses a mocked version of the OP's originally provided filterData. It shows the different behaviors of throttle and debounce for lodash and of two basic custom throttle/debounce implementations ...

    /*function filterData(substr) {
      fetch(url)
        .then(response => response.json())
        .then(data => {
          let filteredData = data.filter(person => person.name.includes(substr));
          print(filteredData);
        });
    }*/
    function filterData(substr) {
      new Promise((resolve/*, reject*/) => {
      
        setTimeout(() => {
          resolve({
            search: substr,
            matches: [substr + 'Bar', substr + ' bazz', substr + ' Foooo']
          });
        }, 300);
    
      }).then(response => console.log({ response }));
    }
    
    function handleSearch(evt) {
      // console.log('this :', this);
      // // return filterData(this.value);
      return filterData(evt.target.value);
    }
    
    document
      .querySelector("#basicThrottled")
      .addEventListener('input', basicThrottle(handleSearch, 500));
    document
      .querySelector("#basicDebounced")
      .addEventListener('input', basicDebounce(handleSearch, 500));
    
    document
      .querySelector("#lodashThrottled")
      .addEventListener('input', _.throttle(handleSearch, 500));
    document
      .querySelector("#lodashDebounced")
      .addEventListener('input', _.debounce(handleSearch, 500));
    body { margin: 0; }
    [type="search"] { min-width: 24%; max-width: 24%; }
    .as-console-wrapper { min-height: 85%; }
    <input id="basicThrottled" type="search" placeholder="basic throttled ..." />
    <input id="basicDebounced" type="search" placeholder="basic debounced ..." />
    
    <input id="lodashThrottled" type="search" placeholder="lodash throttled ..." />
    <input id="lodashDebounced" type="search" placeholder="lodash debounced ..." />
    
    <script>
    function basicDebounce(proceed, delay = 300, target) {
      let timeoutId = null;
    
      return function debounced(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(proceed.bind(target ?? this), delay, ...args);
      };
    }
    function basicThrottle(proceed, threshold = 200, target) {
      let timeoutId = null;
      let referenceTime = 0;
    
      return function throttled(...args) {
        const currentTime = Date.now();
    
        if (currentTime - referenceTime >= threshold) {
          clearTimeout(timeoutId);
    
          referenceTime = currentTime;
          const trigger = proceed.bind((target ?? this), ...args);
    
          timeoutId = setTimeout((() => {
    
            referenceTime = 0;
            trigger();
    
          }), threshold);
    
          trigger();
        }
      };
    }
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>