Context

Qwik provides a context API, very similar to React's functional useContext(). In fact, Qwik's context API is the most efficient way to pass data down, reducing overhead, generating less code, and allowing Qwik to treeshake unused data more heavily.

Qwik's context API is made of 3 methods, importable from @builder.io/qwik:

  • createContext(contextName: string): Context
  • useContextProvider(ctx: Context, value: VALUE): Context
  • useContext(ctx: Context): VALUE

Example

This example is made of two components, the Parent and the Child. The parent component creates some new state and assigns it to a context, allowing any descendent component to get a reference to the "state".

At the same time, the parent component is rendering <div>Count: {state.count}</div>, creating a reactive subscription that will re-render when the Child changes the value in the click handler: <button onClick$={() => state.count++}>.

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

// Create a new context descriptor
export const MyContext = createContext('my-context');

export const Parent = component$(() => {
  // Create some reactive storage
  const state = useStore({
    count: 0,
  });

  // Assign value (state) to the context (MyContext)
  useContextProvider(MyContext, state);
  return (
    <>
      <Child />
      <div>Count: {state.count}</div>
    </>
  );
});

export const Child = component$(() => {
  // Get reference to state using MyContext
  const state = useContext(MyContext);
  return (
    <>
      <button onClick$={() => state.count++}>Increment</button>
    </>
  );
});

Let's dig into every API involved:

API

createContext()

This method takes a string that gives the context unique name, if your application uses a lot of different context or you are writing a component library, we recommend to follow a name convention that avoids conflicts, such as:

export const QwikCityContext = createContext('io.builder.qwik.city');

Notice that the value returned by createContext() does not hold any state, and it's immutable. It's only used to describe the name and type of the context, like an address or an identifier.

Because it does not hold any state, it's ok to call it and make it a singleton, exported in some shared module.

useContextProvider()

Like all use- methods, it can only be called at the root of component$() (not inside branches). This method is called by some higher level component and it's what assigns (provides) a value to the context. The provided value will not be globally available across the whole render tree, but only to descendant components in the tree.

The value passed to useContextProvider() can be any primitive, object (including from useStore), or array that contains serializable values.

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

  const plainArray = listOfUSPresidents();
  useContextProvider(MyContextArray, plainArray);

  const appName = 'My super app';
  useContextProvider(MyContextString, appName);

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

Let's see how Children can consume the values:

useContext()

Again, like all use- methods, they are only usable at the root of a component$(). This methods allows us to get the provided value to a named context.

export const Children = component$(() => {
  const reactiveObject = useContext(MyContextReactive);
  const plainArray = useContext(MyContextArray);
  const appName = useContext(MyContextString);

  return (
    <>
      <div>Child components can use any of the provided values, such as {appName}</div>
    </>
  );
});

Typed contexts

When a context is created using createContext(), a type can be provided. In fact, it's highly recommended to do so, in order to reduce errors and typos.

export interface SharedState {
  count: number;
}
export const MyContext = createContext<SharedState>('my-context');

This way, when using MyContext in useContextProvider() and useContext(), the provided value will have the SharedState type.

Let's see a working example:

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

export interface SharedState {
  count: number;
}
export const MyContext = createContext<SharedState>('my-context');

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

  useContextProvider(MyContext, state); // type checker will ensure the second param is SharedState
  return (
    <>
      <Child />
      <div>Count: {state.count}</div>
    </>
  );
});

export const Child = component$(() => {
  const state = useContext(MyContext); // type of "state" will be `SharedState`
  return (
    <>
      <button onClick$={() => state.count++}>Increment</button>
    </>
  );
});
Made with โค๏ธ by