Vue

Getting started

The @urql/vue bindings have been written with Vue 3 in mind and use Vue's newer Composition API. This gives the @urql/vue bindings capabilities to be more easily integrated into your existing setup() functions.

Installation

Installing @urql/vue is quick and no other packages are immediately necessary.

yarn add @urql/vue graphql
# or
npm install --save @urql/vue graphql

Most libraries related to GraphQL also need the graphql package to be installed as a peer dependency, so that they can adapt to your specific versioning requirements. That's why we'll need to install graphql alongside @urql/vue.

Both the @urql/vue and graphql packages follow semantic versioning and all @urql/vue packages will define a range of compatible versions of graphql. Watch out for breaking changes in the future however, in which case your package manager may warn you about graphql being out of the defined peer dependency range.

Setting up the Client

The @urql/vue package exports a method called createClient which we can use to create the GraphQL client. This central Client manages all of our GraphQL requests and results.

import { createClient } from '@urql/vue';
const client = createClient({
url: 'http://localhost:3000/graphql',
});

At the bare minimum we'll need to pass an API's url when we create a Client to get started.

Another common option is fetchOptions. This option allows us to customize the options that will be passed to fetch when a request is sent to the given API url. We may pass in an options object or a function returning an options object.

In the following example we'll add a token to each fetch request that our Client sends to our GraphQL API.

const client = createClient({
url: 'http://localhost:3000/graphql',
fetchOptions: () => {
const token = getToken();
return {
headers: { authorization: token ? `Bearer ${token}` : '' },
};
},
});

Providing the Client

To make use of the Client in Vue we will have to provide from a parent component to its child components. This will share one Client with the rest of our app. In @urql/vue there are two different ways to achieve this.

The first method is to use @urql/vue's provideClient function. This must be called in any of your parent components and accepts either a Client directly or just the options that you'd pass to createClient.

<script>
import { createClient, provideClient } from '@urql/vue';
const client = createClient({
url: 'http://localhost:3000/graphql',
});
provideClient(client);
</script>

Alternatively we may use the exported install function and treat @urql/vue as a plugin by importing its default export and using it as a plugin.

import { createApp } from 'vue';
import Root from './App.vue';
import urql from '@urql/vue';
const app = createApp(Root);
app.use(urql, {
url: 'http://localhost:3000/graphql',
});
app.mount('#app');

The plugin also accepts createClient's options or a Client as its inputs.

Queries

We'll implement queries using the useQuery function from @urql/vue.

Run a first query

For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items. Let's dive right into it!

<template>
<div v-if="fetching">
Loading...
</div>
<div v-else-if="error">
Oh no... {{error}}
</div>
<div v-else>
<ul v-if="data">
<li v-for="todo in data.todos">{{ todo.title }}</li>
</ul>
</div>
</template>
<script>
import { useQuery } from '@urql/vue';
export default {
setup() {
const result = useQuery({
query: `
{
todos {
id
title
}
}
`
});
return {
fetching: result.fetching,
data: result.data,
error: result.error,
};
}
};
</script>

Here we have implemented our first GraphQL query to fetch todos. We see that useQuery accepts options and returns a tuple. In this case we've set the query option to our GraphQL query. The tuple we then get in return is an array that contains a result object and a re-execute function.

The result object contains several properties. The fetching field indicates whether we're currently loading data, data contains the actual data from the API's result, and error is set when either the request to the API has failed or when our API result contained some GraphQLErrors, which we'll get into later on the "Errors" page.

All of these properties on the result are derived from the shape of OperationResult and are marked as reactive , which means they may update while the query is running, which will automatically update your UI.

Variables

Typically we'll also need to pass variables to our queries, for instance, if we are dealing with pagination. For this purpose useQuery also accepts a variables input, which we can use to supply variables to our query.

<template>
...
</template>
<script>
import { useQuery } from '@urql/vue';
export default {
props: ['from', 'limit'],
setup({ from, limit }) {
return useQuery({
query: `
query ($from: Int!, $limit: Int!) {
todos(from: $from, limit: $limit) {
id
title
}
}
`,
variables: { from, limit }
});
}
};
</script>

As when we're sending GraphQL queries manually using fetch, the variables will be attached to the POST request body that is sent to our GraphQL API.

All inputs that are passed to useQuery may also be reactive state. This means that both the inputs and outputs of useQuery are reactive and may change over time.

<template>
<ul v-if="data">
<li v-for="todo in data.todos">{{ todo.title }}</li>
</ul>
<button @click="from += 10">Next Page</button>
</template>
<script>
import { useQuery } from '@urql/vue';
export default {
setup() {
const from = ref(0);
const result = useQuery({
query: `
query ($from: Int!, $limit: Int!) {
todos(from: $from, limit: $limit) {
id
title
}
}
`,
variables: { from, limit: 10 }
});
return {
from,
data: result.data,
};
}
};
</script>

Pausing useQuery

In some cases we may want useQuery to execute a query when a pre-condition has been met, and not execute the query otherwise. For instance, we may be building a form and want a validation query to only take place when a field has been filled out.

Since with Vue 3's Composition API we won't just conditionally call useQuery we can instead pass a reactive pause input to useQuery.

In the previous example we've defined a query with mandatory arguments. The $from and $limit variables have been defined to be non-nullable Int! values.

Let's pause the query we've just written to not execute when these variables are empty, to prevent null variables from being executed. We can do this by computing pause to become true whenever these variables are falsy:

import { reactive } from 'vue'
import { useQuery } from '@urql/vue';
export default {
props: ['from', 'limit'],
setup({ from, limit }) {
return useQuery({
query: `
query ($from: Int!, $limit: Int!) {
todos(from: $from, limit: $limit) {
id
title
}
}
`,
variables: { from, limit },
pause: computed(() => !from.value || !limit.value)
});
}
};
</script>

Now whenever the mandatory $from or $limit variables aren't supplied the query won't be executed. This also means that result.data won't change, which means we'll still have access to our old data even though the variables may have changed.

It's worth noting that depending on whether from and limit are reactive or not you may have to change how pause is computed. But there's also an imperative alternative to this API. Not only does the result you get back from useQuery have an isPaused ref, it also has pause() and resume() methods.

<template>
<div v-if="fetching">
Loading...
</div>
<button @click="isPaused ? resume() : pause()">Toggle Query</button>
</template>
<script>
import { useQuery } from '@urql/vue';
export default {
setup() {
return useQuery({
query: `
{
todos {
id
title
}
}
`
});
}
};
</script>

This means that no matter whether you're in or outside of setup() or rather supplying the inputs to useQuery or using the outputs, you'll have access to ways to pause or unpause the query.

Request Policies

As has become clear in the previous sections of this page, the useQuery hook accepts more options than just query and variables. Another option we should touch on is requestPolicy.

The requestPolicy option determines how results are retrieved from our Client's cache. By default this is set to cache-first, which means that we prefer to get results from our cache, but are falling back to sending an API request.

In total there are four different policies that we can use:

  • cache-first (the default) prefers cached results and falls back to sending an API request when no prior result is cached.
  • cache-and-network returns cached results but also always sends an API request, which is perfect for displaying data quickly while keeping it up-to-date.
  • network-only will always send an API request and will ignore cached results.
  • cache-only will always return cached results or null.

The cache-and-network policy is particularly useful, since it allows us to display data instantly if it has been cached, but also refreshes data in our cache in the background. This means though that fetching will be false for cached results although an API request may still be ongoing in the background.

For this reason there's another field on results, result.stale, which indicates that the cached result is either outdated or that another request is being sent in the background.

Request policies aren't specific to urql's Vue bindings, but are a common feature in its core. You can learn more about request policies on the API docs.

Reexecuting Queries

The useQuery hook updates and executes queries whenever its inputs, like the query or variables change, but in some cases we may find that we need to programmatically trigger a new query. This is the purpose of the executeQuery method which is a method on the result object that useQuery returns.

Triggering a query programmatically may be useful in a couple of cases. It can for instance be used to refresh data that is currently being displayed. In these cases we may also override the requestPolicy of our query just once and set it to network-only to skip the cache.

import { useQuery } from '@urql/vue';
export default {
setup() {
const result = useQuery({
query: `
{
todos {
id
title
}
}
`
});
return {
data: result.data,
fetching: result.fetching,
error: result.error,
refresh() {
result.executeQuery({
requestPolicy: 'network-only'
});
}
};
}
};
</script>

Calling refresh in the above example will execute the query again forcefully, and will skip the cache, since we're passing requestPolicy: 'network-only'.

Furthermore the executeQuery method can also be used to programmatically start a query even when pause is true, which would usually stop all automatic queries.

Vue Suspense

In Vue 3 a new feature was introduced that natively allows components to suspend while data is loading, which works universally on the server and on the client, where a replacement loading template is rendered on a parent while data is loading.

Any component's setup() function can be updated to instead be an async setup() function, in other words, to return a Promise instead of directly returning its data. This means that we can update any setup() function to make use of Suspense.

The useQuery's returned result supports this, since it is a PromiseLike. We can update one of our examples to have a suspending component by changing our usage of useQuery:

<template>
<ul>
<li v-for="todo in data.todos">{{ todo.title }}</li>
</ul>
</template>
<script>
import { useQuery } from '@urql/vue';
export default {
async setup() {
const { data, error } = await useQuery({
query: `
{
todos {
id
title
}
}
`
});
return { data };
}
};
</script>

As we can see, await useQuery(...) here suspends the component and what we render will not have to handle the loading states of useQuery at all. Instead in Vue Suspense we'll have to wrap a parent component in a "Suspense boundary." This boundary is what switches a parent to a loading state while parts of its children are fetching data. The suspense promise is in essence "bubbling up" until it finds a "Suspense boundary".

<template>
<Suspense>
<template #default>
<MyAsyncComponent />
</template>
<template #fallback>
<span>Loading...</span>
</template>
</Suspense>
</template>

As long as any parent component is wrapping our component which uses async setup() in this boundary, we'll get Vue Suspense to work correctly and trigger this loading state. When a child suspends this component will switch to using its #fallback template rather than its #default template.

Chaining calls in Vue Suspense

As shown above, in Vue Suspense the async setup() lifecycle function can be used to set up queries in advance, wait for them to have fetched some data, and then let the component render as usual.

However, because the async setup() function can be used with await-ed promise calls, we may run into situations where we're trying to call functions like useQuery() after we've already awaited another promise and will be outside of the synchronous scope of the setup() lifecycle. This means that the useQuery (and useSubscription & useMutation) functions won't have access to the Client anymore that we'd have set up using provideClient.

To prevent this, we can create something called a "client handle" using the useClientHandle function.

import { gql, useClientHandle } from '@urql/vue';
export default {
async setup() {
const handle = useClientHandle();
await Promise.resolve(); // NOTE: This could be any await call
const result = await handle.useQuery({
query: gql`
{
todos {
id
title
}
}
`,
});
return { data: result.data };
},
};

As we can see, when we use handle.useQuery() we're able to still create query results although we've interrupted the synchronous setup() lifecycle with a Promise.resolve() delay. This would also allow us to create chained queries by using computed to use an output from a preceding result in a next handle.useQuery() call.

Reading on

There are some more tricks we can use with useQuery. Read more about its API in the API docs for it.

Mutations

The useMutation function isn't dissimilar from useQuery but is triggered manually and accepts only a DocumentNode or string as an input.

Sending a mutation

Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an example! We'll set up a mutation that updates a todo item's title.

import { useMutation } from '@urql/vue';
export default {
setup() {
const { executeMutation: updateTodo } = useMutation(`
mutation ($id: ID!, $title: String!) {
updateTodo (id: $id, title: $title) {
id
title
}
}
`);
return { updateTodo };
},
};

Similar to the useQuery output, useMutation returns a result object, which reflects the data of an executed mutation. That means it'll contain the familiar fetching, error, and data properties — it's identical since this is a common pattern of how urql presents operation results.

Unlike the useQuery hook, the useMutation hook doesn't execute automatically. At this point in our example, no mutation will be performed. To execute our mutation we instead have to call the executeMutation method on the result with some variables.

Using the mutation result

When calling our updateTodo function we have two ways of getting to the result as it comes back from our API. We can either use the result itself, since all properties related to the last operation result are marked as reactive — or we can use the promise that the executeMutation method returns when it's called:

import { useMutation } from '@urql/vue';
export default {
setup() {
const updateTodoResult = useMutation(`
mutation ($id: ID!, $title: String!) {
updateTodo (id: $id, title: $title) {
id
title
}
}
`);
return {
updateTodo(id, title) {
const variables = { id, title: title || '' };
updateTodoResult(variables).then(result => {
// The result is almost identical to `updateTodoResult` with the exception
// of `result.fetching` not being set and its properties not being reactive.
// It is an OperationResult.
});
},
};
},
};

The reactive result that useMutation returns is useful when your UI has to display progress or results on the mutation, and the returned promise is particularly useful when you're adding side-effects that run after the mutation has completed.

Handling mutation errors

It's worth noting that the promise we receive when calling the execute function will never reject. Instead it will always return a promise that resolves to a result.

If you're checking for errors, you should use result.error instead, which will be set to a CombinedError when any kind of errors occurred while executing your mutation. Read more about errors on our "Errors" page.

import { useMutation } from '@urql/vue';
export default {
setup() {
const updateTodoResult = useMutation(`
mutation ($id: ID!, $title: String!) {
updateTodo (id: $id, title: $title) {
id
title
}
}
`);
return {
updateTodo(id, title) {
const variables = { id, title: title || '' };
updateTodoResult(variables).then(result => {
if (result.error) {
console.error('Oh no!', result.error);
}
});
},
};
},
};

There are some more tricks we can use with useMutation.
Read more about its API in the API docs for it.

Reading on

This concludes the introduction for using urql with Svelte. The rest of the documentation is mostly framework-agnostic and will apply to either urql in general or the @urql/core package, which is the same between all framework bindings. Hence, next we may want to learn more about one of the following to learn more about the internals: