State

State management is an important part of any app. In Qwik, we can differentiate between two types of state, reactive and static:

  1. Static state is anything that can be serialized: a string, number, object, array... anything.
  2. A reactive state on the other hand is created with useSignal() or useStore().

It is important to notice that state in Qwik is not necessarily a local component state, but rather an app state that can be instantiated by any component.

useSignal()

Use useSignal() to create a reactive signal (a form of state). The useSignal() takes an initial value and returns a reactive signal.

The reactive signal returned by useSignal() 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.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Increment {count.value}
    </button>
  );
});

This example above shows how useSignal() can be used in a counter component to keep track of the count. Modifying the count.value property will cause the component to be updated automatically. For instance, when the property is changed in the button click handler as in the example above.

useStore()

Works very similarly to useSignal(), but it takes an object as its initial value and the reactivity extends to nested objects and arrays by default. One can think of a store as a multiple-value signal or an object made of several signals.

Use useStore(initialStateObject) hook to create a reactive object. It takes an initial object (or a factory function) and returns a reactive object.

import { component$, useStore } from '@builder.io/qwik';
 
export default component$(() => {
  const state = useStore({ count: 0, name: 'Qwik' });
 
  return (
    <>
      <button onClick$={() => state.count++}>Increment</button>
      <p>Count: {state.count}</p>
      <input
        value={state.name}
        onInput$={(_, el) => (state.name = el.value)}
      />
    </>
  );
});

NOTE For reactivity to work as expected, make sure to keep a reference to the reactive object and not only to its properties. e.g. doing let { count } = useStore({ count: 0 }) and then mutating count won't trigger updates of components that depend on the property.

Because useStore() tracks deep reactivity, that means that Arrays and Objects inside the store will also be reactive.

import { component$, useStore } from '@builder.io/qwik';
 
export default component$(() => {
  const store = useStore({
    nested: {
      fields: { are: 'also tracked' },
    },
    list: ['Item 1'],
  });
 
  return (
    <>
      <p>{store.nested.fields.are}</p>
      <button
        onClick$={() => {
          // Even though we are mutating a nested object, this will trigger a re-render
          store.nested.fields.are = 'tracked';
        }}
      >
        Clicking me works because store is deep watched
      </button>
      <br />
      <button
        onClick$={() => {
          // Because store is deep watched, this will trigger a re-render
          store.list.push(`Item ${store.list.length}`);
        }}
      >
        Add to list
      </button>
      <ul>
        {store.list.map((item, key) => (
          <li key={key}>{item}</li>
        ))}
      </ul>
    </>
  );
});

Notice that for useStore() to track all nested properties, it needs to allocate a lot of Proxy objects. This can be a performance issue if you have a lot of nested properties. In that case, you can use the deep: false option to only track the top-level properties.

const shallowStore = useStore(
  {
    nested: {
      fields: { are: 'also tracked' }
    },
    list: ['Item 1'],
  },
  { deep: false }
);

Methods

To provide methods on the store, you must make them into QRLs and refer to the store with this, like so:

import { component$, useStore, $, type QRL } from "@builder.io/qwik";
 
type CountStore = { count: number; increment: QRL<(this: CountStore) => void> };
 
export default component$(() => {
  const state = useStore<CountStore>({
    count: 0,
    increment: $(function (this: CountStore) {
      this.count++;
    }),
  });
 
  return (
    <>
      <button onClick$={() => state.increment()}>Increment</button>
      <p>Count: {state.count}</p>
    </>
  );
});

Computed state

In Qwik there are two ways to create computed values, each with a different use case (in order of preference):

  1. useComputed$(): useComputed$() is the preferred way of creating computed values. Use it when the computed value can be derived synchronously purely from the source state (current application state). For example, creating a lowercase version of a string or combining first and last name into a full name.

  2. useResource$(): useResource$() is used when the computed value is asynchronous or the state comes from outside of the application. For example, fetching the current weather (external state) based on a current location (application internal state).

In addition to the two ways of creating computed values described above, there is also a lower-level (useTask$()). This way does not produce a new signal, but rather modifies the existing state or produces a side effect.

useComputed$()

Use useComputed$ allows to memoize a value derived synchronously from other state.

It is similar to memo in other frameworks, since it will only recompute the value when one of the input signals changes.

import { component$, useComputed$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const name = useSignal('Qwik');
  const capitalizedName = useComputed$(() => {
    // it will automatically reexecute when name.value changes
    return name.value.toUpperCase();
  });
 
  return (
    <>
      <input type="text" bind:value={name} />
      <p>Name: {name.value}</p>
      <p>Capitalized name: {capitalizedName.value}</p>
    </>
  );
});

NOTE Because useComputed$() is synchronous it is not necessary to explicitly track the input signals.

useResource$()

Use useResource$() to create a computed value that is derived asynchronously. It's the asynchronous version of useComputed$(), which includes the state of the resource (loading, resolved, rejected) on top of the value.

A common use case for useResource$() is to fetch data from an external API within the component itself, this means that the execution might happen in the server or the client.

The useResource$ hook is meant to be used with <Resource />. The <Resource /> component is a convenient way to render different UI based on the state of the resource.

import {
  component$,
  Resource,
  useResource$,
  useSignal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const prNumber = useSignal('3576');
 
  const prTitle = useResource$<string>(async ({ track }) => {
    // it will run first on mount (server), then re-run whenever prNumber changes (client)
    // this means this code will run on the server and the browser
    track(() => prNumber.value);
    const response = await fetch(
      `https://api.github.com/repos/QwikDev/qwik/pulls/${prNumber.value}`
    );
    const data = await response.json();
    return data.title as string;
  });
 
  return (
    <>
      <input type="number" bind:value={prNumber} />
      <h1>PR#{prNumber}:</h1>
      <Resource
        value={prTitle}
        onPending={() => <p>Loading...</p>}
        onResolved={(title) => <h2>{title}</h2>}
      />
    </>
  );
});

Note: The important thing to understand about useResource$ is that it executes on the initial component render (just like useTask$). Often time it is desirable to start fetching the data on the server as part of the initial HTTP request before the component is rendered. Fetching data as part of SSR is a more common and preferred way of loading data and is done through routeLoader$ API instead. useResource$ is more of a low-level API that is useful when you want to fetch data in the browser.

In many ways useResource$ is similar to useTask$. The big differences are:

  • useResource$ allows you to return a "value".
  • useResource$ does not block rendering while the resource is being resolved.

See routeLoader$ for fetching data early as part of initial HTTP request.

NOTE: During SSR the <Resource> component will pause rendering until the resource is resolved. This way the SSR will not render with the loading indicator.

Advanced example

A more complete example of fetching data with AbortController, track and cleanup. This example will fetch a list of jokes based on the query typed by the user, automatically reacting to changes in the query, including aborting requests that are currently pending.

import {
  component$,
  useResource$,
  Resource,
  useSignal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const query = useSignal('busy');
  const jokes = useResource$<{ value: string }[]>(
    async ({ track, cleanup }) => {
      track(() => query.value);
      // A good practice is to use `AbortController` to abort the fetching of data if
      // new request comes in. We create a new `AbortController` and register a `cleanup`
      // function which is called when this function re-runs.
      const controller = new AbortController();
      cleanup(() => controller.abort());
 
      if (query.value.length < 3) {
        return [];
      }
 
      const url = new URL('https://api.chucknorris.io/jokes/search');
      url.searchParams.set('query', query.value);
 
      const resp = await fetch(url, { signal: controller.signal });
      const json = (await resp.json()) as { result: { value: string }[] };
 
      return json.result;
    }
  );
 
  return (
    <>
      <label>
        Query: <input bind:value={query} />
      </label>
      <button>search</button>
      <Resource
        value={jokes}
        onPending={() => <>loading...</>}
        onResolved={(jokes) => (
          <ul>
            {jokes.map((joke, i) => (
              <li key={i}>{joke.value}</li>
            ))}
          </ul>
        )}
      />
    </>
  );
});

As we see in the example above, useResource$() returns a ResourceReturn<T> object that works like a reactive promise, containing the data and the resource state.

The state resource.loading can be one of the following:

  • false - the data is not yet available.
  • true - the data is available. (Either resolved or rejected.)

The callback passed to useResource$() runs right after the useTask$() callbacks complete. Please refer to the Lifecycle section for more details.

<Resource />

<Resource /> is a component meant to be used with the useResource$() that renders different content depending on if the resource is pending, resolved, or rejected.

<Resource
  value={weatherResource}
  onPending={() => <div>Loading...</div>}
  onRejected={() => <div>Failed to load weather</div>}
  onResolved={(weather) => {
    return <div>Temperature: {weather.temp}</div>;
  }}
/>

It is worth noting that <Resource /> is not required when using useResource$(). It is just a convenient way to render the resource state.

This example shows how useResource$ is used to perform a fetch call to the agify.io API. This will guess a person's age based on the name typed by the user, and will update whenever the user types in the name input.

import {
  component$,
  useSignal,
  useResource$,
  Resource,
} from '@builder.io/qwik';
 
export default component$(() => {
  const name = useSignal<string>();
 
  const ageResource = useResource$<{
    name: string;
    age: number;
    count: number;
  }>(async ({ track, cleanup }) => {
    track(() => name.value);
    const abortController = new AbortController();
    cleanup(() => abortController.abort('cleanup'));
    const res = await fetch(`https://api.agify.io?name=${name.value}`, {
      signal: abortController.signal,
    });
    return res.json();
  });
 
  return (
    <section>
      <div>
        <label>
          Enter your name, and I'll guess your age!
          <input onInput$={(ev, el) => (name.value = el.value)} />
        </label>
      </div>
      <Resource
        value={ageResource}
        onPending={() => <p>Loading...</p>}
        onRejected={() => <p>Failed to person data</p>}
        onResolved={(ageGuess) => {
          return (
            <p>
              {name.value && (
                <>
                  {ageGuess.name} {ageGuess.age} years
                </>
              )}
            </p>
          );
        }}
      />
    </section>
  );
});

Passing state

One of the nice features of Qwik is that the state can be passed to other components. Writing to the store will then only re-render the components which read from the store only.

There are two ways to pass state to other components:

  1. pass state to child component explicitly using props,
  2. or pass state implicitly through context.

Using props

The simplest way to pass the state to other components is to pass it through props.

import { component$, useStore } from '@builder.io/qwik';
 
export default component$(() => {
  const userData = useStore({ count: 0 });
  return <Child userData={userData} />;
});
 
interface ChildProps {
  userData: { count: number };
}
export const Child = component$<ChildProps>(({ userData }) => {
  return (
    <>
      <button onClick$={() => userData.count++}>Increment</button>
      <p>Count: {userData.count}</p>
    </>
  );
});

Using context

The context API is a way to pass state to components without having to pass it through props (i.e.: avoids prop drilling issues). Automatically, all the descendant components in the tree can access a reference to the state with read/write access to it.

Check the context API for more information.

import {
  component$,
  createContextId,
  useContext,
  useContextProvider,
  useStore,
} from '@builder.io/qwik';
 
// Declare a context ID
export const CTX = createContextId<{ count: number }>('stuff');
 
export default component$(() => {
  const userData = useStore({ count: 0 });
 
  // Provide the store to the context under the context ID
  useContextProvider(CTX, userData);
 
  return <Child />;
});
 
export const Child = component$(() => {
  const userData = useContext(CTX);
  return (
    <>
      <button onClick$={() => userData.count++}>Increment</button>
      <p>Count: {userData.count}</p>
    </>
  );
});

noSerialize()

Qwik ensures that all application state is always serializable. This is important to ensure that Qwik applications have a resumability property.

At times it is necessary to store data that can't be serialized, noSerialize() tells Qwik not even bother trying to serialize the marked value.

For example a reference to a third-party library such as Monaco editor will always need noSerialize(), as it is not serializable.

If a value is marked as non-serializable then that value will not survive serialization events such as resuming the application on the client from server SSR. In such a situation the value will be set to undefined and it is up to the developer to re-initialize the value on the client.

import {
  component$,
  useStore,
  useSignal,
  noSerialize,
  useVisibleTask$,
  type NoSerialize,
} from '@builder.io/qwik';
import type Monaco from './monaco';
import { monacoEditor } from './monaco';
 
export default component$(() => {
  const editorRef = useSignal<HTMLElement>();
  const store = useStore<{ monacoInstance: NoSerialize<Monaco> }>({
    monacoInstance: undefined,
  });
 
  useVisibleTask$(() => {
    const editor = monacoEditor.create(editorRef.value!, {
      value: 'Hello, world!',
    });
    // Monaco is not serializable, so we can't serialize it as part of SSR
    // We can however instantiate it on the client after the component is visible
    store.monacoInstance = noSerialize(editor);
  });
  return <div ref={editorRef}>loading...</div>;
});

Contributors

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

  • nnelgxorz
  • the-r3aper7
  • voluntadpear
  • kawamataryo
  • JaymanW
  • RATIU5
  • manucorporat
  • literalpie
  • fum4
  • cunzaizhuyi
  • zanettin
  • ChristianAnagnostou
  • shairez
  • forresst
  • almilo
  • Craiqser
  • XiaoChengyin
  • gkatsanos
  • adamdbradley
  • mhevery
  • wtlin1228
  • AnthonyPAlicea
  • sreeisalso
  • wmertens
  • nicvazquez
  • mrhoodz
  • eecopa
  • fabian-hiller
  • julianobrasil
  • aivarsliepa
  • Balastrong