In this tutorial you learn:
useTask$
lifecycle hook worksThe first step is the creation of an input text that bind a Signal, so they will be always in sync:
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.
useSignal
hook: const inputSig = useSignal('');
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:
<input bind:value={inputSig} />
<h1>{inputSig.value}</h1>
Here the full example:
import { component$, useSignal } from '@builder.io/qwik';
export default component$( () => {
const inputSig = useSignal('');
return (
<>
<input bind:value={inputSig} />
<h1>{inputSig.value}</h1>
</>
);
});
Result:
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:
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.
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:
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:
useTask$
function is invoked each time the tracked value changes: inputSig
setTimeout
is created and invoke a function after 1 secondcleanup
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:
Signal
sWe can also update another Signal inside the useTask$
, as shown below.
This script will update and display debouncedSig
after a second user finishes typing:
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>
</>
);
});
We can also invoke another function after the debounce time:
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));
});
// ...
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:
// ...
// 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));
});
// ...
useDebounce
HookSince 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:
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 timeFUNCTION_TO_INVOKE
: the function you want invoke after debouncinguseDebouce
CUSTOM HOOKBelow the full code of the useDebounce
hook, that requires 3 params:
Signal
: the signal we want to track.number
: the milliSeconds to apply for the debounce.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.
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;
}
First I want to show you how we can use our custom hook to simply display a debounced Signal value:
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:
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:
// ...
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 on my YouTube Channel or LinkedIn for more tips and tutorials about front-end