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.

Usually the life of a component starts on the server (during SSR or SSG), in that case, the hooks will run like this:

  useTask$ --> RENDER --> useClientEffect$
                       |
| ------ SERVER ------ | ---- BROWSER ---- |
                       |
                  pause|resume

Notice that because the component was mounted in the server, only useClientEffect$() 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.

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 --> useClientEffect$

| --------------- BROWSER --------------- |

Notice that the lifecycle is exactly the same, but this time all the hooks run in the browser, and non 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$, useStore, useTask$ } from '@builder.io/qwik';

export const Cmp = component$(() => {
  const store = useStore({
    count: 1,
    doubleCount: 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(() => store.count);
    store.doubleCount = 2 * count;
  });

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

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

Notice that useTask$() runs BEFORE the actual rendering and in the server, so if you need to do any DOM manipulation, use useClientEffect$() 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 useClientEffect$)
  • 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 store = useStore({
    users: [],
  });
  useTask$(async () => {
    if (isServer) {
      // If the component is mounted on the server, call DB directly.
      store.users = await db.requestUsers();
    }
    if (isBrowser) {
      // If the component is mounted on the browser, fetch users through API.
      store.users = await fetchGetUsers();
    }
  });

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

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

useClientEffect$()

  • When: AFTER component's first render and on tracked state changes
  • Times: at least once
  • Platform: browser only

For all components that got mounted during SSR (in the server), the useClientEffect$() will run eagarly, that means, without user interaction, but the eagerness can be configured!

useClientEffect$(() => console.log('runs in the browser'), {
  eagerness: 'visible', // '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:

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

Example

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

NOTE: Don't abuse useClientEffect$() 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, useClientEffect$() is probably not the right answer.

When to use useClientEffect$()

Use this hook when you need to run JS right at loading time of the page, even if the user never interacts with the page.

For example:

  • Read the DOM after rendering
  • Initialize some animations
  • WebGL logic
  • Use DOM APIs like localStorage
  • Run some code without user interaction, like a setInterval()

How does it compare with React's useEffect()?

Both APIs share a lot of semantics, but while both run AFTER rendering, useClientEffect$() can run also independently from rendering.

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