qwik-urql
v0.4.2
Published
Urql support for Qwik projects
Downloads
7
Readme
Qwik Urql ⚡️
A small library to use Urql with Qwik.
- :white_check_mark: Query & mutation hooks
- :white_check_mark: SSR
- :white_check_mark: Lazy loaded client
- :white_check_mark: Auth tokens
- :white_check_mark: Abort signals
- :white_check_mark: Re-execute queries (see example app buttons)
- :white_check_mark: Reactive cache / watch for changes
- :white_check_mark::eight_pointed_black_star: Optimistic response. Works except for the first optimistic update after SSR.
- :hourglass: 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 asuseQuery
. 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 thedata
,errors
,loading
state, and a method to execute the mutationmutate$
. 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