State

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

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

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

useSignal()

const signal = useSignal(initialState) is a hook that creates a reactive signal. It takes an initial value and returns a reactive signal.

The reactive signal returned by useSignal() consist of an object with a single property (i.e.: signal.value). If you change the value property of the object, any component that depends on it will be updated.

Example

This example shows how useSignal() can be used in a counter component to keep track of the count.

export default component$(() => {
  const count = useSignal(0);

  return (
    <>
      <button onClick$={() => count.value++}>Increment</button>
      Count: {count.value}
    </>
  );
});

Just accessing the count.value property will cause the component to be updated if the value of the signal changes. For instance, when the property is changed in the button click handler.

useStore()

Works very similary to useSignal(), but it takes an object as its initial value. One can think of a store as a multiple-value signal, or an object made of several signals.

const store = useStore(initialState) is a hook that creates a reactive object. It takes an initial object, and returns a reactive object.

The reactive object returned by useStore() is just like any other object, but it is reactive. If you change any property of the object, any component that depends on that property will be updated.

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.

Example

This example shows how useStore() can be used in a counter component to keep track of the count.

export default component$(() => {
  const state = useStore({ count: 0 });

  return (
    <>
      <button onClick$={() => state.count++}>Increment</button>
      Count: {state.count}
    </>
  );
});

Just accessing the state.count property will cause the component to be updated if the count changes. For instance, when the property is changed in the button click handler.

Recursive values

By default, useStore() only tracks the top level fields in your store, which means that for any updates to be registered, you have to update values at the top level field.

THIS EXAMPLE WILL NOT WORK:

import { component$, useStore } from '@builder.io/qwik';

export default component$(() => {
  const store = useStore({
    nested: { fields: { are: 'not tracked' } },
  });

  return (
    <>
      <p>{store.nested.fields.are}</p>
      <button onClick$={() => (store.nested.fields.are = 'tracked')}>Click me</button>
    </>
  );
});

In order for the updates to be registered with the default tracking strategy, we would have to update the top level nested field like so:

store.nested = { fields: { are: { "tracked" } } }

In order to make the first example work, we can pass a second argument to useStore(), and tell it to use recursion to track all the values in our store, no matter how deep.

export default component$(() => {
  const store = useStore(
    {
      nested: { fields: { are: 'not tracked' } },
    },
    { recursive: true }
  );

  return (
    <>
      <p>{store.nested.fields.are}</p>
      <button onClick$={() => (store.nested.fields.are = 'tracked')}>Click me</button>
    </>
  );
});

Now, the component will update as expected. This will also track individual values inside of arrays!

import { component$, useStore } from '@builder.io/qwik';

export default component$(() => {
  const store = useStore(
    {
      letters: ['A', 'B', 'C'],
    },
    { recursive: true }
  );

  return (
    <>
      {store.letters.map((letter) => (
        <p>{letter}</p>
      ))}
      <button
        onClick$={() => {
          store.letters[2] = 'Z';
        }}
      >
        Click me
      </button>
    </>
  );
});

Passing a store to other components

One of the nice features of Qwik is that the state can be passed to other components, and both can read and write to it, allowing data to flow across the tree in all directions.

There are two ways to pass state to other components: with props, or with context.

Using props

The most simple way to pass state to other components is to pass it as props. This is the way you would do it in React, and it works in Qwik as well.

export const Parent = component$(() => {
  const userData = useStore({
    count: 0,
  });

  return (
    <>
      <Child userData={userData} />
    </>
  );
});

export const Child = component$(({ userData }) => {
  return (
    <>
      <button onClick$={() => userData.count++}>Increment</button>
      Count: {userData.count}
    </>
  );
});

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.

export const CTX = createContextId('stuff');

export const Stores = component$(() => {
  const userData = useStore({
    count: 0,
  });

  useContextProvider(CTX, userData);

  return (
    <>
      <Child />
    </>
  );
});

export const Child = component$(() => {
  const userData = useContext(CTX);
  return (
    <>
      <button onClick$={() => userData.count++}>Increment</button>
      Count: {userData.count}
    </>
  );
});

Computed values

Computed values are values that are derived from other values. They are useful when you have a value that is derived from other values, and you want to keep it in sync with the other values.

In Qwik there are two ways to create computed values, using useTask$() or useResource$().

The main difference between the two is that useTask$() allows side effects and the execution is serialized, while useResource$() is asynchronous and multiple useResource$() calls can happen in parallel.

useTask$() is usually used to compute intermediate state, while useResource$() has better ergonomics to compute the final state, used for rendering. Let's see some examples:

useTask$() example

export default component$(() => {
  const state = useStore({
    count: 0,
    doubleCount: 0,
  });

  useTask$(({ track }) => {
    track(() => state.count);
    state.doubleCount = state.count * 2;
  });

  return (
    <>
      <button onClick$={() => state.count++}>Increment</button>
      Count: {state.count}
      Count * 2: {state.doubleCount}
    </>
  );
});

useResource$() example

export default component$(() => {
  const state = useStore({
    count: 0,
  });

  const doubleCount = useResource$(({ track }) => {
    track(() => state.count);
    return state.count * 2;
  });

  return (
    <>
      <button onClick$={() => state.count++}>Increment</button>
      Count: {state.count}
      Count * 2: {doubleCount.promise}
    </>
  );
});

Both useTask() and useResource() are explained in detail in their respective sections.

Reactivity

Thanks to the fine grained reactivity of Qwik, only the components that depend on the state will be updated. This is a huge performance gain, as only the components that need to be updated will be updated.

noSerialize()

Sometimes, you may want to store a value in the state, but you don't want it to be serialized. This is useful for storing values that are not serializable, like functions, or classes.

import { component$, useStore, noSerialize, useVisibleTask$ } from '@builder.io/qwik';

export const App = component$(() => {
  const state = useStore({
    monacoInstance: null,
  });

  useVisibleTask$(() => {
    // Monaco is not serializable, so we can't store it in the state using `noSerialize()`
    state.monacoInstance = noSerialize(new Monaco());
  });
  return <></>;
});
Made with โค๏ธ by