Qwik emote

Qwik

How to create a custom reusable hook: debounced Input

How to create a debounced input in Qwik and a generic useDebounce custom hook

2023-08-12
10 min read
Difficulty
qwik
Qwik emote

In this tutorial you learn:

  • how to create a debounced input in Qwik
  • how useTask$ lifecycle hook works
  • how to create a custom hook in Qwik

TOPICS

Input Binding

The first step is the creation of an input text that bind a Signal, so they will be always in sync:

What is a Signal?

A Signal is one of the strategy to handle a "reactive" state in Qwik.

It consists of an object with a single property .value. If you change the value property of the signal, any component that depends on it will be updated automatically.

  1. First create a new "Signal" by using the Qwik useSignal hook:
QwikTypeScript
  const inputSig = useSignal('');
  1. Use bind:value to create a 2-way binding with an input:

The bind attribute is a convenient API to two-way data bind the value of a <input /> to a Signal:

QwikTypeScript
<input bind:value={inputSig} />
  1. Now we can use the signal in the JSX template and it will always display the current input value:
QwikTypeScript
<h1>{inputSig.value}</h1>

Here the full example:

QwikTypeScript
import { component$, useSignal } from '@builder.io/qwik';

export default component$( () => {
  const inputSig = useSignal('');

  return (
    <>
      <input bind:value={inputSig} />
      <h1>{inputSig.value}</h1>
    </>
  );
});

Result:

Simple Debounce

In the previous example the signal is updated each time the user writes something in the input. However, our goal is to run an action only after a certain amount of time, for example we could update another Signal, invoke a function or invoke a REST API only when user finishes typing.

We can easily achieve this goal by "tracking" (watching) the value of the signal, that I remind you it will currently always be in sync with it.

How?

By using the useTask$ Qwik lifecycle hook and watching the signal with its track property:

QwikTypeScript
import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export default component$( () => {
  const inputSig = useSignal('');

  // invoked each time user types something
  useTask$(({ track }) => {
    track(() => inputSig.value);
    console.log(inputSig.value)
  });

  return (
    <>
      <input bind:value={inputSig} />
    </>
  );
});

Now we could also update another signal (or do anything else) when the input types something.

QwikTypeScript
import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export default component$(() => {
  const inputSig = useSignal('');
  // Create another signal
  const anotherSig = useSignal('');

  useTask$(({ track }) => {
    track(() => inputSig.value);
    // update another signal
    anotherSig.value = inputSig.value;
  });

  return (
    <>
      <h3>Update another signal</h3>
      <input bind:value={inputSig} placeholder="write something" />
      <pre>{anotherSig.value}</pre>
    </>
  );
});

The problem with the previous script is that we update the anotherSig signal too many times, each time users types something.

Now imagine if you wanted to update the signal, or invoke a function, only when the user finishes typing.

One of the most used techniques to solve this problem is the debounce strategy, i.e. delaying the operation for a few milliseconds with a setTimeout and destroying it after each keystroke to make it start over.

Below is a simple example to create a debounce in Qwik, that is very similar to any other debounce examples you will find for React or other libs/frameworks:

QwikTypeScript
import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export default component$( () => {
  const inputSig = useSignal('');

  useTask$(({ track, cleanup }) => {
    track(() => inputSig.value);

    // 1. Create a setTimeout to delay the console.log
    const debounced = setTimeout(() => {
      console.log(' do something ')
    }, 1000);

    // 2. destroy the setTimeout each time
    // the "tracked" value (inputSig) changes
    // (and when the component is unmount too)
    cleanup(() => clearTimeout(debounced));
  });

  return (
    <>
      <input bind:value={inputSig} />
    </>
  );
});

In the snippet above:

  • the useTask$ function is invoked each time the tracked value changes: inputSig
  • a setTimeout is created and invoke a function after 1 second
  • the cleanup function is invoked and the timeout is destroyed every time the inputSig signal changes (and also when the component is unmount)

In fact, as you can see in the image below, the console.log is shown one second after the end of typing:

Debounced Signals

We can also update another Signal inside the useTask$, as shown below.

This script will update and display debouncedSig after a second user finishes typing:

QwikTypeScript
import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export default component$( () => {
  const inputSig = useSignal('');
  // 1. Create a new Signal to contain the debounced value
  const debouncedSig = useSignal('');

  useTask$(({ track, cleanup }) => {
    track(() => inputSig.value);

    const debounced = setTimeout(() => {
      // 2. Update the signal
      debouncedSig.value = inputSig.value;
    }, 1000);
    cleanup(() => clearTimeout(debounced));
  });

  return (
    <>
      <input bind:value={inputSig} />
      <h1>{debouncedSig.value}</h1>
    </>
  );
});

Invoke a Function

We can also invoke another function after the debounce time:

QwikTypeScript
  import { component$, useSignal, useTask$ } from '@builder.io/qwik';
  // ...

  // 1. create the function
  const doSomething = $(() => {
    console.log(' do something ')
  })

  useTask$(({ track, cleanup }) => {
    track(() => inputSig.value);

    const debounced = setTimeout(() => {
      debouncedSig.value = inputSig.value;
      // 2. invoke the function
      doSomething()
    }, 1000);
    cleanup(() => clearTimeout(debounced));
  });

  // ...

What is the dollar `$` sign?

The doSomething() function is wrapped by a $ sign. Why?

Qwik splits up your application into many small pieces we call symbols. A component can be broken up into many symbols, so a symbol is smaller than a component. The splitting up is performed by the Qwik Optimizer.


The $ suffix is used to signal both the optimizer and the developer when this transformation occurs.

...and we can also pass some parameters to the function:

QwikTypeScript

  // ...

  // 1. The function now accepts the "value" parameter
  const doSomething = $((value: string) => {
    console.log('do something', value)
  })

  useTask$(({ track, cleanup }) => {
    track(() => inputSig.value);

    const debounced = setTimeout(() => {
      debouncedSig.value = inputSig.value;
      // 2. Pass the input value to the function
      doSomething(inputSig.value)
    }, 1000);
    cleanup(() => clearTimeout(debounced));
  });

  // ...

Custom useDebounce Hook

Since this debounce logic makes the code less readable and will probably be reused many times in our application, it is convenient to create a custom hook that can be easily reused.

Our goal is to be able to use it like this:

QwikTypeScript
 useDebounceFn(SIGNAL_TO_TRACK, DELAY_TIME, FUNCTION_TO_INVOKE)

This hook requires 3 params:

  • SIGNAL_TO_TRACK: the signal to "track" / "watch"
  • DELAY_TIME: milliseconds to apply as debounce time
  • FUNCTION_TO_INVOKE: the function you want invoke after debouncing

useDebouce CUSTOM HOOK

Below the full code of the useDebounce hook, that requires 3 params:

  • A Signal: the signal we want to track.
  • A number: the milliSeconds to apply for the debounce.
  • A PropFunction<(value: T) => void>: the function to invoke after the debounce time. This is a special type provided by Qwik to define a function wrapped by the $ sign with one param of type T that returns nothing (void).

As you can see we have simply moved the logic from the component to this custom hook that will invoke the passed function (fn) after an amount of time (milliSeconds) and it will also updates and returns the debouncedSig signal.

QwikTypeScript
use-debounce.tsx
import { type PropFunction, type Signal, useSignal, useTask$ } from '@builder.io/qwik';

export function useDebounce<T>(
  signal: Signal,
  milliSeconds: number,
  fn?: PropFunction<(value: T) => void>,
) {
  // create the debounced Signal
  const debouncedSig = useSignal('');

  useTask$(({ track, cleanup }) => {
    // track the signal
    track(() => signal.value);

    // start timeout
    const debounced = setTimeout(async () => {
      // 1. invoke the function
      await fn(signal.value)
      // 2. update the debouncedSig signal
      debouncedSig.value = signal.value;
    }, milliSeconds);

    // clean setTimeout each time the tracked signal changes
    cleanup(() => clearTimeout(debounced));
  });

  // Return the debouncedSig
  return debouncedSig;
}

USAGE

First I want to show you how we can use our custom hook to simply display a debounced Signal value:

QwikTypeScript
Any Component or custom hook
import { component$, useSignal, $ } from '@builder.io/qwik';
// 1. import the hook
import { useDebounce } from '../hooks/use-debounce';

export default component$(() => {
  const inputSig = useSignal('');

  // 2. this Signal is updated after a second
  const debouncedSig = useDebounce(inputSig, 1000);

  return (
    <>
      <h3>Custom Hook: Debounced Value</h3>
      <input bind:value={inputSig} placeholder="write something" />
      {/* 3. Display the debounced value */}
      <pre>{debouncedSig.value}</pre>
    </>
  );
});

In the next snippet we'll invoked a debounced function instead:

QwikTypeScript
Any Component or custom hook
import { $, component$, useSignal } from '@builder.io/qwik';
// 1. import the hook
import { useDebounce } from './use-debounce';

export default component$( () => {
  const inputSig = useSignal('');
  const debouncedSig = useSignal('');

// 3. the function invoked after the debounce time
  const doSomething = $((value: string) => {
    console.log('do something', value)
  })

  // 2. invoke the function after 1 sec
  useDebounce(inputSig, 1000, loadSomething);

  return (
    <>
      <input bind:value={inputSig} />
      <h1>{debouncedSig.value}</h1>
    </>
  );
});

Anyway, if you prefer, you can also use an inline function:

QwikTypeScript
Any Component or custom hook
  // ...
  useDebounce(inputSig, $((value: string) => {
    doSomething(value)
  }), 1000)
  // ...
});

Now you can use this custom hook every time the user fills in an input field and you want to invoke a function after typing.

You can find all the examples in this StackBlitz.

Follow me for more tips

Follow me on my YouTube Channel or LinkedIn for more tips and tutorials about front-end

Keep updated about latest content
videos, articles, tips and news
BETA