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 changesRender
- runs afterTASK
and beforeVisibleTask
VisibleTask
- runs afterRender
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, useuseVisibleTask$()
instead, which runs on the client after rendering.
useTask$()
when you need to
Use - 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 ofuseTask$
, consider usinguseResource$()
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 usesrequestIdleCallback()
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}</>;
});
useVisibleTask$()
When to use NOTE: Don't abuse
useVisibleTask$()
when the same logic can be achieved usinguseTask$()
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
}
}