State

State management is an important part of any app. In Qwik, there are 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 '@qwik.dev/core';
 
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.

NOTE If you just need to read the signal's value, don't pass the entire signal as a prop, instead pass only its value:

Avoid:

const isClosedSig = useSignal(false);
 
return <Child isClosed={isClosedSig} />;

Instead do:

const isClosedSig = useSignal(false);
 
return <Child isClosed={isClosedSig.value} />;

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 '@qwik.dev/core';
 
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 '@qwik.dev/core';
 
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 + 1}`);
        }}
      >
        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 }
);

Handling Dynamic Object Mutations

When dynamically manipulating object properties, such as deleting them while they are being rendered somewhere in the app, you may encounter issues. This could happen if the component renders values dependent on an object's property that is currently being removed. To prevent this situation, use optional chaining when accessing properties. For example, if attempting to remove a property:

delete store.propertyName;

Be sure to access this property cautiously in the component by using optional chaining ( ?. ):

const propertyValue = store.propertyName?.value;

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 "@qwik.dev/core";
 
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>
    </>
  );
});

Do you know why you should use a regular function(){} instead of an arrow function in useStore()? This is because arrow functions don't have their own bindings to this in JavaScript. This means that if you try to access this using an arrow function, this.count could point to another object's count 😱.

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 names into a full name.

  2. useAsync$(): useAsync$() is the preferred way for asynchronous computed values. Use it 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), or performing a long computation in a WebWorker.

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$ 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 '@qwik.dev/core';
 
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.

useComputed$ and noSerialize()

When using useComputed$() with noSerialize(), the computed value will not be sent to the client. This means that the value will be recalculated on the client when it is needed.

You can use this to instantiate a custom class.

useAsync$()

useAsync$() is similar to useComputed$(), but it allows the compute function to be asynchronous. It returns an AsyncSignal<T> that has the result of the async function. The common use case is to fetch data asynchronously, possibly based on other signals (you need to read those with track()).

A common use case is to fetch data from an external API within the component, which can occur either on the server or the client.

Example

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$, useAsync$, useSignal } from '@qwik.dev/core';
 
export default component$(() => {
  const query = useSignal('');
 
  // this will first run during SSR (server)
  // then re-run whenever postId changes (client)
  // so this code runs both on the server and the client
  const jokes = useAsync$(async ({ track, abortSignal }) => {
    const url = new URL(
      'https://v2.jokeapi.dev/joke/Programming?safe-mode&amount=2'
    );
    const search = track(query);
    if (search) {
      url.searchParams.set('contains', search);
    }
 
    // The abortSignal is automatically aborted when this function re-runs,
    // canceling any pending fetch requests.
    const resp = await fetch(url, { signal: abortSignal });
    const json = (await resp.json()) as {
      jokes: { setup?: string; delivery?: string; joke?: never }[];
    };
 
    return json.jokes;
  });
 
  return (
    <>
      <label>
        Query: <input bind:value={query} />
      </label>
      {jokes.loading ? (
        <p>Loading...</p>
      ) : jokes.error ? (
        <div>Error: {jokes.error.message}</div>
      ) : jokes.value ? (
        <ul>
          {jokes.value.map((joke, i) => (
            <li key={i}>
              <div style={{ whiteSpace: 'pre-wrap' }}>
                {joke.joke || `${joke.setup}\n${joke.delivery}`}
              </div>
            </li>
          ))}
        </ul>
      ) : (
        <p>No jokes found</p>
      )}
    </>
  );
});

As we see in the example above, useAsync$() returns an AsyncSignal<T> object that works like a reactive promise, containing the data and the async state.

Note: The important thing to understand about useAsync$ is that it executes on the initial component render (just like useTask$). Often times it is desirable to start fetching the data on the server as part of the initial HTTP request, even before the component is rendered. Fetching data based on the path as part of Server-Side Rendering (SSR) is a common and preferred method of data loading, typically handled by the routeLoader$ API. useAsync$ is more of a low-level API that is useful when you want to fetch data in the browser.

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

  • useAsync$ allows you to return a "value" and exposes it via the AsyncSignal.
  • useAsync$ does not block rendering while the value is being resolved.
  • useAsync$ provides reactive .loading and .error properties, which can be used to show loading spinners or error messages in the UI. useAsync$ also provides an abortSignal that can be used to cancel the async operation when the component is destroyed or when the async function is re-run due to a change in tracked state. useAsync$ has built-in support for polling with interval, which allows you to re-run the async function at a specified interval, as long as it is used somewhere. useTask$ on the other hand is useful for running any code that doesn't need to return a value or directly affect the rendering of the component, implementing logic based on reactive state.

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

Channel example

In this example, we use useAsync$() to get updates from a server using Server-Sent Events (SSE). Note that it writes to its own value, updating its subscribers. To be able to refer to itself, the signal needs to be in an object.

import { component$, useAsync$ } from '@qwik.dev/core';
 
export default component$(() => {
  const _ref = {} as { state: AsyncSignal<string> };
  _ref.state = useAsync$<string[]>(
    ({ track, abortSignal }) => {
      const url = new URL('/api/state-channel');
      const eventSource = new EventSource(url);
 
      eventSource.onmessage = (event) => {
        _ref.state.value = event.data;
      };
 
      abortSignal.addEventListener('abort', () => {
        eventSource.close();
      });
 
      return 'init';
    },
    // Close the channel as soon as there are no subscribers
    { eagerCleanup: true }
  );
  const state = _ref.state;
 
  return <div>Current state is: {state.value}</div>;
});

API

The API of useAsync$() (and createAsync$()) is as follows:

function useAsync$<T>(
  asyncFn: (opts: {
    track: <U>(signal: Signal<U>) => U;
    cleanup: (fn: () => void) => void;
    abortSignal: AbortSignal;
    previous: T | undefined;
  }) => Promise<T>,
  options?: { initial?: T; interval?: number; concurrency?: number; timeout?: number; eagerCleanup?: boolean }
): AsyncSignal<T>;

Arguments:

  • The track and cleanup functions are the same as in useTask$, used to track reactive signals and clean up resources when the async function is re-run or the component is destroyed.

  • The abortSignal is automatically aborted when the async function is re-run or the component is destroyed, which allows you to cancel any pending async operations. Best practice is to pass it to every API that supports it. Also, read from it after every await, and if it is aborted, stop the execution of the async function. (if (abortSignal.aborted) return;)

  • previous is the previous resolved value of the async function, which can be useful to implement logic based on the previous value.

The callback passed to useAsync$() runs as soon as it is read. That means that if you use it in the JSX output, it will start during the initial render of that part of the JSX. So if you pass it to a component, it will start when that component is rendered, after tasks have run. If you want to start it immediately on initialization, you can call asyncSignal.promise() to start it immediately. This can also be used to await the completion, but you still have to read the value or error from the signal itself.

Options:

  • initial: the initial value of the async signal before the async function resolves. This is useful to show some initial data while the async function is still loading. If not provided, reading the value before it's resolved will throw a Promise, which is Qwik's way of ensuring the current function will re-run when the value is available.
  • interval: if provided, the async function will be re-run every interval milliseconds, as long as the async signal is used somewhere. This is useful to keep data fresh.
  • concurrency: controls what happens when the async function is still running and it needs to be re-run (because of a change in tracked signals or because of polling).
    • The default is concurrency: 1, which means that the currently running async function and its cleanups have to complete before a new one starts.
    • If set to a number, there can be be up to that number currently in flight before new calls need to wait for one of them to complete. 0 means no limit.
    • No matter the concurrency setting, whenever the signal is invalidated, the currently running async function will be aborted using the abortSignal. It's up to you to decide to use the signal or not.
    • The cleanups are awaited, so if you do not want that, you should not return a Promise.
  • timeout: if provided, the async function will be automatically aborted if it takes longer than the specified time to resolve.
  • eagerCleanup: if set to true, the async function will be aborted as soon as there are no subscribers to the async signal, instead of waiting for the next time it needs to be re-run. This is useful to free up resources as soon as they are not needed anymore.

The returned AsyncSignal<T> has the following (reactive) properties:

interface AsyncSignal<T> {
  value: T;
  loading: boolean;
  error: Error | undefined;
  interval: number;
  promise(): Promise<void>;
  abort(reason?: any): void;
}
  • value: T: the resolved value (or initial value if not yet resolved). Note that reading this property before the async function has completed will throw a Promise for the result, which causes Qwik to wait until that resolves and then re-run the reading function. Writing a different value to this property will update the value and notify subscribers.
  • loading: boolean: whether the async function is currently running
  • error: Error | undefined: the error if the async function failed
  • interval: number: the current polling interval in milliseconds. This can be updated to change the polling interval or to stop polling by setting it to 0.
  • promise(): Promise<void>: a function that can be called to start the async function. It returns a promise that resolves when the async function completes, but not its result. That needs to be read from value or error.
  • abort(reason?: any): void: a function that can be called to abort the currently running async function and run cleanups if needed. The reason is passed to the abortSignal.

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 '@qwik.dev/core';
 
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 '@qwik.dev/core';
 
// 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>
    </>
  );
});

useSerializer$() / createSerializer$()

Sometimes you need to serialize data that Qwik doesn't know how to serialize. For this, you can use the useSerializer$(cfg) hook to create a serializer for the data. createSerializer$(cfg) does the same thing but is not bound to a specific component and can be called anywhere.

The cfg argument is an object with generic type S (serialized data) and T (the object type) with the following properties:

  • deserialize: (data: S) => T: Creates the object using the initial value or the deserialized value.
  • serialize?: (value: T) => S | Promise<S>: Optional, serializes the object. If not provided, the object will be serialized as undefined.
  • initial?: S: Optional, the initial value of the serializer.
  • update?: (value: T) => T | void: Optional, updates the object when the reactive state changes. This can only be passed when cfg is a function, see below.

The result is a SerializerSignal<T> object. This is a lazy reactive signal that contains the serialized value of the object. It must be passed around as the signal, otherwise serialization will not work.

import { component$, useSerializer$ } from '@qwik.dev/core';
import { MyClass } from './my-class';
 
export default component$(() => {
  const serializer = useSerializer$({
    deserialize: (data) => new MyClass(data),
    serialize: (value) => value.serialize(),
    initial: {x: 3, y: 4},
  });
 
  return <div>{JSON.stringify(serializer.value)}</div>;
});

During SSR, deserialize will be called with the initial value. You can then use signal.value to access the custom object. On the client, the deserialize function will be called with the serialized value, but only when the signal is first read. So if you don't need to read the signal, no code will run.

You can also use reactive state to create a serializer. For this, you need to pass the config as a function that captures the reactive state. Then you can use the update function to update the object when the reactive state changes. If the update function returns an object, it will be used as the new value for the serializer, and it will trigger all listeners. The trigger happens even when returning the same object.

import { component$, useSerializer$ } from '@qwik.dev/core';
import { ComplexObject } from './complex-object';
 
export default component$(() => {
  const sig = useSignal(123);
  const serializer = useSerializer$((cfg) => ({
    deserialize: () => new ComplexObject(sig.value),
    update: (obj) => {
      if (sig.value < 7) {
        // ignore changes below 7
        return;
      }
      // Tell the object about the change
      obj.update(sig.value);
      // Return the updated object so the listeners are notified
      return obj;
    }
  }));
 
  // ...
});

This primitive is very powerful, and can be used to create all sorts of custom serializers. Note that the serialize function is allowed to return a Promise, so you could for example write the data to a database and return the id. Note that you will need to use if (isServer) to guard this operation, so vite won't bundle database code in the client bundle.

Also note that serialization can also happen on the client, when calling server$ functions.

noSerialize() / NoSerializeSymbol

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

Sometimes, it's necessary to store data that can't be serialized; noSerialize() instructs Qwik not to attempt serializing 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.

NoSerializeSymbol is an alternative to noSerialize(): You can add this symbol to an object (preferably on the prototype) and it will mark the object as non-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 SSR. In this 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 '@qwik.dev/core';
import type Monaco from './monaco';
import { monacoEditor } from './monaco';
 
class MyClass {
  [NoSerializeSymbol] = true;
}
 
export default component$(() => {
  const editorRef = useSignal<HTMLElement>();
  const store = useStore<{ monacoInstance: NoSerialize<Monaco>, myClass: MyClass }>({
    monacoInstance: undefined,
    myClass: 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);
    // Here we demonstrate `NoSerializeSymbol` for the same purpose
    store.myClass = new MyClass();
  });
  return <div ref={editorRef}>loading...</div>;
});

SerializerSymbol

The SerializerSymbol is a symbol that can be added to an object to achieve custom serialization. There is no corresponding DeserializerSymbol however, for that you should use the useSerializer$() hook.

This can be used to skip serializing part of an object, or to work with third-party objects that are not serializable by default.

import { component$, SerializerSymbol } from '@qwik.dev/core';
import { getItem, type Item } from './my-db-layer';
 
const serializer = (o: Item) => o.toJSON();
type MyItem = Item & { [SerializerSymbol]: typeof serializer };
 
export default component$((props: {id: string}) => {
  const obj = useSignal<MyItem | null>(null);
  useTask$(async () => {
    const item = await getItem(props.id);
    if (item) {
      item[SerializerSymbol] = serializer;
      obj.value = item;
    }
  });
  return <div>{JSON.stringify(obj.value)}</div>;
});

You could even monkey-patch the third-party library to add the SerializerSymbol to their objects. Note that this can result in problems when the third-party library is updated, so use this method with caution. You will also need to update the types so that our linting plugin doesn't complain about serialization of unknown types.

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
  • Jemsco
  • shairez
  • ianlet