Lifecycles

Thanks to Resumability, components life and its lifecycle extend across server and browser. Sometimes the component will be first rendered in the server, sometimes it could be in the browser, however, in both cases the lifecycle will be the same, only its execution happens in different environments.

In Qwik, there is only 3 lifecycles stages:

  • Task - runs before rendering and when tracked state changes
  • 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 where the component is initially rendered.

Additionally, this task can be reactive and re-execute when some tracked signal or store changes, like this:

useTask$(({ track }) => {
  track(() => store.count);
  // will run when the component mounts and every time "store.count" changes
});

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

  useTask$(track state) -> RENDER -> CLICK (state changes) -> RE-RUN useTask$(track state)
  โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”            |                          โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”
| --------- 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 another words, tasks execute sequentially even if they are asynchronous. (Only one task executes at a time / Tasks block rendering).

Example

The useTask$() function is used to observe the state.count property. Any changes to the state.count cause the arrow function to execute which in turn updates the state.doubleCount to the double of state.count.

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

export const Cmp = component$(() => {
  const count = useSignal(1);
  const doubleCount = useSignal(0);

  // this task will be called exactly once, either on the server or on the browser
  useTask$(() => {
    console.log('component mounted');
  });

  // this task will be called at mount and every time `store.count` changes
  useTask$(({ track }) => {
    const count = track(() => count.value);
    doubleCount.value = 2 * count;
  });

  return (
    <div>
      {count.value} / {doubleCount.value}
    </div>
  );
});

The example above uses the track function to watch changes in () => count.value. The callback will run once in the SSR when the component is mounted and every time count.value changes.

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

Use useTask$() when you need to

  • Run async tasks before rendering
  • Run code only once before component is first rendered
  • Compute derived state from tracked state (without causing potential re-rendering like in useVisibleTask$)
  • Programmatically run code when state changes

Note, if you're thinking about load 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.

Server/Browser only task

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.

import { isServer, isBrowser } from '@builder.io/qwik/build';

export const Cmp = component$(() => {
  const users = useSignal([]);
  useTask$(async () => {
    if (isServer) {
      // If the component is mounted on the server, call DB directly.
      users.value = await db.requestUsers();
    }
    if (isBrowser) {
      // If the component is mounted on the browser, fetch users through API.
      users.value = await fetchGetUsers();
    }
  });

  return (
    <>
      {users.value.map((user) => (
        <User user={user} />
      ))}
    </>
  );
});

But as mentioned above - for data fetching, consider using useResource$() instead.

useVisibleTask$()

  • When: once the component becomes visible on the browser, and when tracked state changes
  • Times: at least once
  • Platform: browser only

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 signal or store changes, like this:

useVisibleTask$(({ track }) => {
  track(() => store.count);
  // will run when the component becomes visible and every time "store.count" changes
  console.log('runs in the browser');
});

Under the hood, useVisibleTask$() creates a DOM listener that uses Intersection Observer to detect when the component becomes visible in the viewport.

However this strategy can be changed, and for example, run when the document is ready.

useVisibleTask$(() => console.log('runs in the browser'), {
  strategy: 'document-ready', // 'load' | 'visible' | 'idle'
});

This is a unique feature of Qwik, any other frameworks would execute this and other code as part of hydration, but in Qwik, you can actually specify when it happens:

  • "intersection-observer": (this is the default) when the component becomes visible in the viewport (uses Intersection Observer under the hood).
  • "document-ready": when the documents finish loading (the document's "load" event)
  • "document-idle": after load the first moment the site becomes idle. It uses requestIdleCallback() under the hood.

Example

export const Timer = component$(() => {
  const count = useSignal(0);
  useVisibleTask$(() => {
    // Only runs in the client
    const timer = setInterval(() => {
      count.value++;
    }, 500);
    return () => {
      clearInterval(timer);
    };
  });
  return <>{count.value}</>;
});

When to use useVisibleTask$()

NOTE: Don't abuse useVisibleTask$() when the same logic can be achieved using useTask$() or other means. Ask to yourself: Does this code really need to run at the beginning in the browser? If the answer is no, useVisibleTask$() is probably not the right answer.

As the name implies, useVisibleTask$() is useful when you need to run some code when the component becomes visible in the viewport. This is useful for:

  • Run code BEFORE user interaction, like animations, or other logic that needs to run before user interaction.
  • Read the DOM after rendering
  • Initialize some animations
  • WebGL logic

Use Method 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
  }
}
Made with โค๏ธ by