qwik-urql
TypeScript icon, indicating that this package has built-in type declarations

0.4.2 • Public • Published

Qwik Urql ⚡️

A small library to use Urql with Qwik.

  • Query & mutation hooks
  • SSR
  • Lazy loaded client
  • Auth tokens
  • Abort signals
  • Re-execute queries (see example app buttons)
  • Reactive cache / watch for changes
  • ✴️ Optimistic response. Works except for the first optimistic update after SSR.
  • Code generators

Setup

This is the minimal setup required for standard Query/Mutations. See the reactive cache section for watch queries

Create a new file to hold your Urql client configuration under src/client.ts and export a factory for your client.

import { createClient } from '@urql/core';

export const clientFactory = () => {
  return createClient({
    url: 'http://localhost:3000/graphql',
    exchanges: [/** ... */],
  });
};

Now provide the client in your root.tsx component and wrap the client in a QRL to ensure it is lazy loaded.

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

export default component$(() => {
  return (
    <UrqlProvider client={$(clientFactory)}>
      <QwikCity>
        <head></head>
        <body lang='en'>
          ...
        </body>
      </QwikCity>
    </UrqlProvider>
  );
});

Queries

First compile the GQL and then call useQuery. The result is a Qwik ResourceReturn which can be used with the <Resource /> component.

import { component$, JSXNode, Resource, $ } from '@builder.io/qwik';
import { gql, OperationResult } from '@urql/core';
import { useQuery } from 'qwik-urql';

// Create a GQL query. This will not get serialized because it is only
// referenced in a QRL
export const query = gql`
  query Item($id: String!) {
    item(id: $id) {
      id
      title
    }
  }
`;

// Using a QRL we can drastically reduce the initial bundle size.
export const Query = $(() => query)

export default component$(() => {
  const vars = useStore({ id: '...' })
  const query = useQuery(Query, vars, { requestPolicy: 'network-only' });

  return <Resource
    value={query}
    onPending={...}
    onRejected={...}
    onResolved={...}
  />
})

Mutations

There are 2 hooks for running a mutation.

  • The useMutationResource works the exact same as useQuery. It will trigger as soon as the component loads. You can then re-trigger it by changing the input store.
  • The useMutation returns a store that includes the data, errors, loading state, and a method to execute the mutation mutate$. This allows you to delay the execution of the request until a user interaction happens.
export const mutation = gql`
  mutation UpdateItem($id: String!, $title: String!) {
    item(id: $id, title: $title) {
      id
      title
    }
  }
`;

export const Mutation = $(() => mutation)

export default component$(() => {
  // You can pass in variables during initialisation or execution
  const initialVars = useStore({ id: '...' })
  const { data, errors, loading, mutate$ } = useMutation(Mutation, initialVars);

  return <>
    { loading ? 'loading' : 'done' }
    <button onClick$={() => mutate$({ title: '...' })}>Mutate</button>
  </>
})

SSR

Qwik doesn't hydrate on the client after SSR. This means we don't need to support the SSR exchange, everything works without it.

Reactive cache

Install the QwikExchange to enable subscriptions.

To set this up, add the qwikExchange to your client and make sure it is before the cache exchange. All queries will be reactive by default.

By default, all queries will create subscriptions. This can be turned off per request by passing in watch: false to the query context, or pass in a global default to the UrqlProvider options prop.

import { createClient, dedupExchange, fetchExchange } from '@urql/core';
import { cacheExchange } from '@urql/exchange-graphcache';
import { qwikExchange, ClientFactory } from 'qwik-urql';

export const clientFactory: ClientFactory = ({ qwikStore }) => {
  return createClient({
    url: 'http://localhost:3000/graphql',
    exchanges: [
      qwikExchange(qwikStore),
      dedupExchange,
      cacheExchange({}),
      fetchExchange,
    ],
  });
};

Authentication

Make sure you follow the latest recommendations by Urql.

First update your clientFactory to include the Urql auth exchange. Notice the factory now accepts an authTokens parameter which can be used when making your requests.

export const clientFactory: ClientFactory = ({ authTokens }) => {
  const auth = authExchange<UrqlAuthTokens>({
    getAuth: async ({ authState }) => {
      if (!authState) {
        if (authTokens) {
          return authTokens;
        }

        return null;
      }

      return null;
    },
    willAuthError: ({ authState }) => {
      if (!authState) return true;
      return false;
    },
    addAuthToOperation: ({ authState, operation }) => {
      if (!authState || !authState.token) {
        return operation;
      }

      const fetchOptions =
        typeof operation.context.fetchOptions === 'function'
          ? operation.context.fetchOptions()
          : operation.context.fetchOptions || {};

      return makeOperation(operation.kind, operation, {
        ...operation.context,
        fetchOptions: {
          ...fetchOptions,
          headers: {
            ...fetchOptions.headers,
            Authorization: authState.token,
          },
        },
      });
    },
    didAuthError: ({ error }) => {
      return error.graphQLErrors.some(
        (e) => e.extensions?.code === 'FORBIDDEN'
      );
    },
  });

  return createClient({
    url: 'http://localhost:3000/graphql',
    exchanges: [dedupExchange, cacheExchange({}), auth, fetchExchange],
  });
};

Authentication has to use cookies to allow authenticated SSR. To do this, you will need to set a cookie after your user has logged in. This cookie then needs to be read from the request headers and saved to a Qwik store. (I'll include an example of this with firebase soon)

To inject your auth tokens into the clientFactory, you need to provide them in your root.tsx:

import { UrqlProvider } from 'qwik-urql';

export default component$(() => {
  // Get access to your authentication tokens
  const session = useCookie('session');

  // Add them to a store
  const authState = useStore({ token: session  });

  return (
    // Provide them to your entire app
    <UrqlProvider auth={authState} client={$(clientFactory)}>
      <QwikCity>
        <head>
          <meta charSet='utf-8' />
          <RouterHead />
        </head>
        <body lang='en'>
          <RouterOutlet />
          <ServiceWorkerRegister />
        </body>
      </QwikCity>
    </UrqlProvider>
  );
});

You should now receive auth tokens in your GQL server from both the frontend client and from SSR clients.

Code generation

Coming soon.

I plan to create a code generate to convert .graphql files like this:

query Film($id: String!) {
  film(id: $id) {
    id
    title
  }
}

Into something like this:

import { component$, JSXNode, Resource, $ } from '@builder.io/qwik';
import { gql, OperationResult } from '@urql/core';
import { useQuery } from 'qwik-urql';

export type FilmQueryResponse = {
  film: {
    title: string;
    id: string;
  };
};

export type FilmQueryVars = {
  id: string;
};

export const filmQuery = gql`
  query Film($id: String!) {
    film(id: $id) {
      id
      title
    }
  }
`;

export const FilmQuery = $(() => filmQuery)

export const useFilmQuery = (vars: FilmQueryVars) => {
  return useQuery(FilmQuery, vars);
};

export type FilmResourceProps = {
  vars: FilmQueryVars;
  onResolved$: (
    value: OperationResult<FilmQueryResponse, FilmQueryVars>
  ) => JSXNode;
  onPending$?: () => JSXNode;
  onRejected$?: (reason: any) => JSXNode;
};

export const FilmResource = component$((props: FilmResourceProps) => {
  const vars = props.vars;
  const value = useFilmQuery(vars);

  return (
    <Resource
      value={value}
      onPending={props.onPending$}
      onRejected={props.onRejected$}
      onResolved={props.onResolved$}
    />
  );
});

And then in your component all you need to import is this:

const vars = useStore({ id: '0' });

return <FilmResource
  vars={vars}
  onPending$={() => <div>Loading...</div>}
  onRejected$={() => <div>Error</div>}
  onResolved$={(res) => (
    <>{res.data ? res.data.film.title : 'No results'}</>
  )}
/>

Example app

The example requires this PR for authentication to work. To test authentication you will need to build it yourself and update your node_modules until it is merged

An example app is included in the repository. The source code is found in src/example

pnpm start

Development

Development mode uses Vite's development server. For Qwik during development, the dev command will also server-side render (SSR) the output. The client-side development modules loaded by the browser.

npm run dev

Note: during dev mode, Vite will request many JS files, which does not represent a Qwik production build.

Production

The production build should generate the production build of your component library in (./lib) and the typescript type definitions in (./lib-types).

npm run build

Readme

Keywords

Package Sidebar

Install

npm i qwik-urql

Weekly Downloads

0

Version

0.4.2

License

MIT

Unpacked Size

53.8 kB

Total Files

34

Last publish

Collaborators

  • dustinjsilk