What is a debouncer and why it's important

A debouncer is a programming pattern used to ensure that time-consuming tasks do not fire too frequently, thereby preventing performance issues or the overwhelming of servers with requests. This is particularly useful in scenarios such as search inputs where you may not want to trigger a search request with every keystroke, but rather after the user has paused typing.

By implementing a debouncer, you can enhance both user experience and application efficiency. This is achieved by delaying a function's execution until a specified amount of time has elapsed since its last invocation.

When to use debouncers

Debouncers are best used in situations where you want to limit the frequency of function calls. Common use cases include:

  • User input validation: Delaying validation until the user stops typing to avoid unnecessary processing for every keystroke.
  • Search functionality: Waiting until a user finishes typing a search query to begin the search process, reducing the number of search requests sent to the server.
  • Window resizing: Limiting the number of recalculations or adjustments when the browser window is resized.

How to implement a debouncer in Qwik

The Qwik framework provides unique capabilities for managing state and effects in a way that is both serializable and efficient. Implementing a debouncer in Qwik involves using Qwik's primitives, such as useSignal for state management and $ for marking functions that can capture state without breaking serialization. Below is a simple debouncer pattern. It behaves similarly to closure-based debouncers that execute a function when the timeout is reached.

export const useDebouncer = (fn: QRL<(args: any) => void>, delay: number) => {
  const timeoutId = useSignal<number>();
 
  return $((args: any) => {
    clearTimeout(timeoutId.value);
    timeoutId.value = Number(setTimeout(() => fn(args), delay));
  });
};

This debouncer takes a function and a delay as arguments. It utilizes useSignal to track the timeoutID, ensuring compatibility with Qwik's resumability model and its use of QRL's. The returned function, when called, clears any existing timeout and sets a new one to invoke the provided function after the specified delay.

How to use the debouncer

The example below demonstrates using the debouncer in a component to efficiently manage user input. By debouncing the input, the application updates the state only after the user has stopped typing for 1-second, optimizing performance for operations such as API calls or data filtering.

import { $, useSignal, component$, type QRL } from "@qwik.dev/core";
import { useDebouncer } from "~/utils/debouncer";
 
export default component$(() => {
  const debouncedValue = useSignal("");
 
  const debounce = useDebouncer(
    $((value: string) => {
      debouncedValue.value = value;
    }),
    1000
  );
 
  return (
    <>
      <input
        onInput$={(_, target) => {
          debounce(target.value);
        }}
      />
      <p>{debouncedValue.value}</p>
    </>
  );
});

Live Demo

In the live demo below, useDebouncer is used to update the debouncedValue signal after a 1-second delay has elapsed since the last keystroke.

Bonus: useDebouncer$

To save our developers the extra "wrapping with a $()" action.

We can leverage Qwik's implicit$FirstArg function to create a useDebouncer$ function that automatically wraps the provided function with $().

This is how Qwik actually implements all of its built-in $ hooks.

export const useDebouncerQrl = (fn: QRL<(args: any) => void>, delay: number) => {
  const timeoutId = useSignal<number>();
 
  return $((args: any) => {
    clearTimeout(timeoutId.value);
    timeoutId.value = Number(setTimeout(() => fn(args), delay));
  });
};
 
export const useDebouncer$ = implicit$FirstArg(useDebouncerQrl);

And now we could do:

const debounce = useDebouncer$(
  (value: string) => {
    debouncedValue.value = value;
  },
  1000
);

Pretty great, huh? :)

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • n8sabes
  • KenAKAFrosty
  • shairez