State

Applications need to keep track of values that change over time. In Qwik, those values usually fall into two groups:

  1. Plain data: serializable values like strings, numbers, objects, and arrays.
  2. Reactive state: values that trigger UI updates where they are used. Create it with useSignal() or useStore().

State in Qwik does not have to live inside one component. Any component can create state, and that state can be shared with other parts of the app.

useSignal()

A reactive signal is an object with a single .value property.

When .value changes, Qwik updates the components that use it.

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

In this example, count is a signal. Clicking the button updates count.value, and Qwik updates the button text automatically.

useStore()

A store is similar to a signal, but for an entire object instead of a single .value property.

Pass an initial object, or a function that returns one. Qwik tracks reads and writes to the object's properties. By default, nested objects and arrays are also tracked.

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)}
      />
      <p>Name: {state.name}</p>
    </>
  );
});

Nested objects and arrays

By default, a store also tracks objects and arrays inside it:

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$={() => {
          // Updating a nested property updates the UI.
          store.nested.fields.are = 'tracked';
        }}
      >
        Update nested field
      </button>
      <br />
      <button
        onClick$={() => {
          // Updating an array also updates the UI.
          store.list.push(`Item ${store.list.length + 1}`);
        }}
      >
        Add to list
      </button>
      <ul>
        {store.list.map((item, key) => (
          <li key={key}>{item}</li>
        ))}
      </ul>
    </>
  );
});

Nested tracking is convenient, but it can add overhead for large objects. If you only need to track top-level properties, pass { deep: false }:

const shallowStore = useStore(
  {
    nested: {
      fields: { are: 'not tracked deeply' }
    },
    count: 0,
  },
  { deep: false }
);

Store methods

Stores can include methods. Use $() so the method can be serialized, and use the function keyword when the method needs JavaScript's this keyword.

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>
    </>
  );
});
Why use the function keyword?

This example uses this.count to update the store. Use the function keyword here so this points to the store.

Computed values

Sometimes you do not need to store state directly. You can calculate it from existing state, or resolve it from async work.

useComputed$() and useAsync$() both create signals for those values:

  1. useComputed$(): creates a signal from a synchronous calculation, such as formatting a name.
  2. useAsync$(): creates a signal from async work, such as fetched data or Web Worker output.

If you do not need a new signal, try useTask$() instead. A task runs code during component setup or when tracked state changes, and you write any result to existing state yourself.

useComputed$()

Create a signal for a value that can be calculated immediately from other state.

Qwik keeps the result and runs the function again only when one of the signals it reads changes.

import { component$, useComputed$, useSignal } from '@qwik.dev/core';
 
export default component$(() => {
  const name = useSignal('Qwik');
  const upperName = useComputed$(() => {
    // Runs again when name.value changes.
    return name.value.toUpperCase();
  });
 
  return (
    <>
      <input type="text" bind:value={name} />
      <p>Name: {name.value}</p>
      <p>Uppercase name: {upperName.value}</p>
    </>
  );
});
NOTE

You do not need to list dependencies for useComputed$(). Qwik automatically tracks the signals read inside the function.

useAsync$()

Create a signal for a value that comes from async work, such as fetching data, calling server$(), reading from a Web Worker, or running a long calculation.

Like useComputed$(), the function can run again when the state it tracks changes.

Unlike useComputed$(), the result can be pending. While the async work is running, the signal exposes .loading; when it finishes, it exposes either .value or .error.

Read .value when you need the result. If the result is not ready yet, that part of the UI can wait and a <Suspense> boundary can show fallback UI.

The async work can run during SSR and in the browser. useAsync$() also supports polling for values that need to stay up to date.

Example

Below, useAsync$() fetches programming jokes for the current query. track(query) reruns the async function when the query changes, and abortSignal cancels any previous request.

import { component$, useAsync$, useSignal } from '@qwik.dev/core';
 
type Joke = {
  joke?: string;
  setup?: string;
  delivery?: string;
};
 
export default component$(() => {
  const query = useSignal('');
 
  const jokes = useAsync$(async ({ track, abortSignal }) => {
    // Re-run when query changes.
    const search = track(query).trim();
    const url = new URL(
      'https://v2.jokeapi.dev/joke/Programming?safe-mode&amount=2'
    );
 
    if (search) {
      url.searchParams.set('contains', search);
    }
 
    const response = await fetch(url, { signal: abortSignal });
    const data = (await response.json()) as {
      jokes?: Joke[];
    };
 
    return data.jokes ?? [];
  });
 
  let content;
  if (jokes.loading) {
    content = <p>Loading...</p>;
  } else if (jokes.error) {
    content = <div>Error: {jokes.error.message}</div>;
  } else if (jokes.value.length === 0) {
    content = <p>No jokes found</p>;
  } else {
    content = (
      <ul>
        {jokes.value.map((joke, i) => (
          <li key={i}>
            <div style={{ whiteSpace: 'pre-wrap' }}>
              {joke.joke ?? `${joke.setup}\n${joke.delivery}`}
            </div>
          </li>
        ))}
      </ul>
    );
  }
 
  return (
    <>
      <label>
        Query: <input bind:value={query} />
      </label>
      {content}
    </>
  );
});

The code handles loading and errors inline with .loading and .error. During SSR, Qwik waits for the async work before rendering the result.

useAsync$ and Suspense

The jokes example handles several UI states inline: loading, error, no results, and results.

That is useful when each state needs its own markup. If you only need fallback UI while .value is not ready, wrap that part of the UI in <Suspense>.

Sharing 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>
    </>
  );
});

Serialization

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.

useComputed$() and noSerialize()

If a computed value is wrapped with noSerialize(), Qwik will not send that value to the client. It will be created again on the client when the computed signal is read.

This is useful for values that cannot be serialized, such as custom class instances.

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