Tasks

Tasks are meant for running asynchronous operations as part of component initialization or change of component state.

Note: Tasks are similar to useEffect() in React, but there are enough differences that we did not want to call them the same so as not to bring preexisting expectations about how they work. The main differences are:

  • Tasks are asynchronous.
  • Task run on server and browser.
  • Tasks run before rendering and can block rendering.

useTask$() should be your default go-to API for running asynchronous (or synchronous) work as part of component initialization or state change. It is only when you can't achieve what you need with useTask$() that you should consider using useVisibleTask$() or useResource$().

The basic use case for useTask$() is to perform work on component initialization. useTask$() has these properties:

  • It can run on either the server or in the browser.
  • It runs before rendering and blocks rendering.
  • If multiple tasks are running then they are run sequentially in the order they were registered. An asynchronous task will block the next task from running until it completes.

Tasks can also be used to perform work when a component state changes. In this case, the task will rerun every time the tracked state changes. See: track().

Sometimes a task needs to run only on the browser and after rendering, in that case, you should use useVisibleTask$().

Sometimes a task should fetch data asynchronously and produce a signal (and not block rendering), in that case, you should use useResource$().

Lifecycle

Thanks to Resumability, component lifetime and its lifecycle extend across server and browser runtime. Sometimes the component will first be rendered in the server, and other times it could be rendered in the browser. However, in both cases, the lifecycle (order) will be the same, only its execution location will be in different environments (server vs browser).

Note: For systems that use hydration the execution of the application happens twice. Once on a server (SSR/SSG) and once on the browser (hydration). For this reason, many frameworks have "effects" which only execute on the browser. That means that the code that runs on the server is different than the code that runs on the browser. Qwik execution is unified, meaning if the code has already been executed on the server it does not re-execute it on the browser.

In Qwik, there are only 3 lifecycle stages:

  • Task - run before rendering and also when tracked state changes. (Tasks run sequentially, and block rendering.)
  • Render - runs after TASK and before VisibleTask
  • VisibleTask - runs after Render and when the component becomes visible
      useTask$ -------> RENDER ---> useVisibleTask$
                            |
| --- SERVER or BROWSER --- | ----- BROWSER ----- |
                            |
                       pause|resume

SERVER: Usually the life of a component starts on the server (during SSR or SSG), in that case, the useTask$ and RENDER will run in the server, and then the VisibleTask will run in the browser, after the component is visible.

Notice that because the component was mounted in the server, only useVisibleTask$() runs in the browser. This is because the browser continues the same lifecycle, that was paused in the server right after the render and resumed in the browser.

BROWSER: Sometimes a component will be first mounted/rendered in the browser, for example when the user SPA navigates to a new page, or a "modal" component first appears in the page. In that case, the lifecycle will run like this:

  useTask$ --> RENDER --> useVisibleTask$
 
| -------------- BROWSER --------------- |

Notice that the lifecycle is exactly the same, but this time all the hooks run in the browser, and not in the server.

useTask$()

  • When: BEFORE component's first render, and when tracked state changes
  • Times: at least once
  • Platform: server and browser

useTask$() registers a hook to be executed upon component creation, it will run at least once either in the server or in the browser, depending on where the component is initially rendered.

Additionally, this task can be reactive and will re-execute when tracked state changes.

Notice that any subsequent re-execution of the task will always happen in the browser, because reactivity is a browser-only thing.

                      (state change) -> (re-execute)
                                  ^            |
                                  |            v
 useTask$(track) -> RENDER ->  CLICK  -> useTask$(track)
                        |
  | ----- SERVER ------ | ----------- BROWSER ----------- |
                        |
                   pause|resume

If useTask$() does not track any state, it will run exactly once, either in the server or in the browser (not both), depending where the component is initially rendered. Effectively behaving like an "on-mount" hook.

useTask$() will block the rendering of the component until after its async callback resolves, in other words, tasks execute sequentially even if they are asynchronous. (Only one task executes at a time / Tasks block rendering).

Let's look at the simplest use case of the task to run some asynchronous work on component initialization.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const fibonacci = useSignal<number[]>();
 
  useTask$(async () => {
    const size = 40;
    const array = [];
    array.push(0, 1);
    for (let i = array.length; i < size; i++) {
      array.push(array[i - 1] + array[i - 2]);
      await delay(100);
    }
    fibonacci.value = array;
  });
 
  return <p>{fibonacci.value?.join(', ')}</p>;
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

In this example

  • The useTask$() computes the fibonacci number one entry per 100 ms. So 40 entries take 4 seconds to render.
    • The useTask$() executes on the server as part of the SSR (the result may be cached in CDN.)
    • Because the useTask$() blocks rendering, the rendered HTML page takes 4 seconds to render.
  • Because this task has no track() it will never rerun, making it effectively an initialization code.
    • Because this component only renders on the server, the useTask$() will never run on the browser. (code will never download to browser.)

Notice that useTask$() runs BEFORE the actual rendering and on the server. Therefore if you need to do DOM manipulation, use useVisibleTask$() instead, which runs on the browser after rendering.

Use useTask$() when you need to:

  • Run async tasks before rendering
  • Run code only once before the component is first rendered
  • Programmatically run side-effect code when state changes

Note, if you're thinking about loading data (like using fetch()) inside of useTask$, consider using useResource$() instead. This API is more efficient in terms of leveraging SSR streaming and parallel data fetching.

On mount

Qwik does not have a specific "on-mount" hook because useTask$() without track() effectively behaves like a mount hook.

This is because useTask$ runs always at least once when the component is first mounted.

import { component$, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
 
  useTask$(async () => {
    // A task without `track` any state effectively behaves like a `on mount` hook.
    console.log('Runs once when the component mounts in the server OR client.');
  });
 
  return <div>Hello</div>;
});

One unique aspect of Qwik, is that components are mounted only ONCE across the server and client. This is a property of resumability. What it means, is that if useTask$ runs during SSR it will not run again in the browser because Qwik does not hydrate.

track()

There are times when it is desirable to re-run a task when a component state changes. This is done by using the track() function. The track() function allows you to set up a dependency on a component state on the server (if initially rendered there) and then re-execute the task when the state changes on the browser (the same task will never be executed twice on the server side).

Note: If all you want to do is compute a new state from an existing state synchronously, you should use useComputed$() instead.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const text = useSignal('Initial text');
  const delayText = useSignal('');
 
  useTask$(({ track }) => {
    track(() => text.value);
    const value = text.value;
    const update = () => (delayText.value = value);
    isServer
      ? update() // don't delay on server render value as part of SSR
      : delay(500).then(update); // Delay in browser
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p>Delayed text: {delayText}</p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

On the server:

  • The useTask$() runs on the server and the track() function sets up a subscription on text signal.
  • The page is rendered.

On the browser:

  • The useTask$() does not have to run (or be downloaded) eagerly because Qwik knows that the task is subscribed to the text signal from the server execution.
  • When the user types in the input box, the text signal changes. Qwik knows that the useTask$() is subscribed to the text signal and it is at this time that the useTask$() closure is brought into the JavaScript VM to be executed.

The useTask$()

  • The useTask$() blocks rendering until it completes. If you don't want to block rendering (as in this case) make sure that the task is resolved, and run the delay work on a separate unconnected promise. (In our case we don't await delay(). Doing so would block rendering.)

Sometimes it is required to only run code either in the server or in the client. This can be achieved by using the isServer and isBrowser booleans exported from @builder.io/qwik/build as shown above.

track() as a function

In the above example track() was used to track a specific signal. However, track() can also be used as a function to track multiple signals at once.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const isUppercase = useSignal(false);
  const text = useSignal('');
  const delayText = useSignal('');
 
  useTask$(({ track }) => {
    const value = track(() =>
      isUppercase.value ? text.value.toUpperCase() : text.value.toLowerCase()
    );
    const update = () => (delayText.value = value);
    isServer
      ? update() // don't delay on server render value as part of SSR
      : delay(500).then(update); // Delay in browser
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <label>
        Is uppercase? <input type="checkbox" bind:checked={isUppercase} />
      </label>
      <p>Delay text: {delayText}</p>
    </section>
  );
});
 
function delay(time: number) {
  return new Promise((resolve) => setTimeout(resolve, time));
}

In this example the track() takes a function that not only reads the signal but also transforms its value to uppercase/lowercase. The track() is doing subscription on multiple signals and computes their value.

cleanup()

Sometimes when running a task cleanup work needs to be performed. When a new task is triggered, the previous task's cleanup() callback is invoked. (Also when the component is removed from the DOM then the cleanup() callback is also invoked.)

  • The cleanup() function is not invoked when the task is completed. It is only invoked when a new task is triggered or when the component is removed.
  • The cleanup() function is invoked on the server after the applications are serialized into HTML.
  • The cleanup() function is not transferable from server to browser. (Cleanup is meant to release resources on the VM where it is running. It is not meant to be transferred to the browser.)

This example shows how to implement a debounce feature using the cleanup() function.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const text = useSignal('');
  const debounceText = useSignal('');
 
  useTask$(({ track, cleanup }) => {
    const value = track(() => text.value);
    const id = setTimeout(() => (debounceText.value = value), 500);
    cleanup(() => clearTimeout(id));
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p>Debounced text: {debounceText}</p>
    </section>
  );
});

useVisibleTask$()

Sometimes a task needs to run only on the browser and after rendering, in that case, you should use useVisibleTask$(). The useVisibleTask$() is similar to useTask$() but it only runs on the browser and after initial rendering. useVisibleTask$() registers a hook to be executed when the component becomes visible in the viewport, it will run at least once in the browser, and it can be reactive and re-execute when some tracked state changes.

useVisibleTask$() has these properties:

  • runs on the client only.
  • eagerly executes code on the client when the component becomes visible.
  • runs after initial rendering.
  • does not block rendering.

Caution: The useVisibleTask$() should be used as a last resort, because it eagerly executes code on the client. Qwik through resumability goes out of its way to delay the execution of code on the client, and useVisibleTask$() is an escape hatch that should be used with caution. See Best Practices for more details. If you need to run a task on a client consider useTask$() with a server guard.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const text = useSignal('Initial text');
  const isBold = useSignal(false);
 
  useTask$(({ track }) => {
    track(() => text.value);
    if (isServer) {
      return; // Server guard
    }
    isBold.value = true;
    delay(1000).then(() => (isBold.value = false));
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p style={{ fontWeight: isBold.value ? 'bold' : 'normal' }}>
        Text: {text}
      </p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

In the above example the useTask$() is guarded by isServer. The track() is before the guard which allows the server to set up the subscription but does not execute any code on the server. The client then executes the useTask$() once the text signal changes.

This example shows how to use useVisibleTask$() to initialize a clock on the browser only when the clock component becomes visible.

import {
  component$,
  useSignal,
  useVisibleTask$,
  type Signal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const isClockRunning = useSignal(false);
 
  return (
    <>
      <div style="position: sticky; top:0">
        Scroll to see clock. (Currently clock is
        {isClockRunning.value ? ' running' : ' not running'}.)
      </div>
      <div style="height: 200vh" />
      <Clock isRunning={isClockRunning} />
    </>
  );
});
 
const Clock = component$<{ isRunning: Signal<boolean> }>(({ isRunning }) => {
  const time = useSignal('paused');
  useVisibleTask$(({ cleanup }) => {
    isRunning.value = true;
    const update = () => (time.value = new Date().toLocaleTimeString());
    const id = setInterval(update, 1000);
    cleanup(() => clearInterval(id));
  });
  return <div>{time}</div>;
});

Notice how the clock's useVisibleTask$() does not run until the <Clock> component became visible. The default behavior of useVisibleTask$() is to run the task when the component becomes visible. This behavior is implemented through intersection observers.

Option eagerness

At times it is desirable to run useVisibleTask$() eagerly as soon as the application is loaded in the browser. In that case the useVisibleTask$() needs to run in eager mode. This is done by using the { strategy: 'document-ready' }.

import {
  component$,
  useSignal,
  useVisibleTask$,
  type Signal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const isClockRunning = useSignal(false);
 
  return (
    <>
      <div style="position: sticky; top:0">
        Scroll to see clock. (Currently clock is
        {isClockRunning.value ? ' running' : ' not running'}.)
      </div>
      <div style="height: 200vh" />
      <Clock isRunning={isClockRunning} />
    </>
  );
});
 
const Clock = component$<{ isRunning: Signal<boolean> }>(({ isRunning }) => {
  const time = useSignal('paused');
  useVisibleTask$(
    ({ cleanup }) => {
      isRunning.value = true;
      const update = () => (time.value = new Date().toLocaleTimeString());
      const id = setInterval(update, 1000);
      cleanup(() => clearInterval(id));
    },
    { strategy: 'document-ready' }
  );
  return <div>{time}</div>;
});

In this example the clock starts running immediately on the browser regardless of whether it is visible or not.

Advanced: Time of running, and managing visibility with CSS

Internally, useVisibleTask$ is implemented by adding an attribute on the first rendered component (either the returned component or in case of a Fragment, its first child). With the standard eagerness, this means that if the first rendered component is hidden, the task will not run.

This means that you can use CSS to influence when the task runs. For example, if the task should only run on a mobile device, you can return a <div class="md:invisible" /> (in the case of Tailwind CSS).

This also means you cannot unhide a component using a visible task; for that you can return a Fragment:

return (<>
  <div />
  <MyHiddenComponent hidden={!showSignal.value} />
</>)

Use Hook Rules

When using lifecycle hooks, you must adhere to the following rules:

  • They can only be called at the root level of component$ (not inside conditional blocks)
  • They can only be called at the root of another use* method, allowing for composition.
useHook(); // <-- โŒ does not work
 
export default component$(() => {
  useCustomHook(); // <-- โœ… does work
  if (condition) {
    useHook(); // <-- โŒ does not work
  }
  useTask$(() => {
    useNavigate(); // <-- โŒ does not work
  });
  const myQrl = $(() => useHook()); // <-- โŒ does not work
  return <button onClick$={() => useHook()}></button>; // <-- โŒ does not work
});
 
function useCustomHook() {
  useHook(); // <-- โœ… does work
  if (condition) {
    useHook(); // <-- โŒ does not work
  }
}

Contributors

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

  • mhevery
  • manucorporat
  • wtlin1228
  • AnthonyPAlicea
  • the-r3aper7
  • sreeisalso
  • brunocrosier
  • harishkrishnan24
  • gioboa
  • bodhicodes
  • zanettin
  • blackpr
  • mrhoodz
  • ehrencrona
  • julianobrasil
  • adamdbradley
  • aendel