Queries
You can query GraphQL APIs with the useQuery
composition function after you’ve setup the GraphQL Client.
Queries Basics
The useQuery
function is a composable function that provides query state and various helper methods for managing the query.
To execute a query the useQuery
accepts a GraphQL query as the first argument. The query
property is a string
containing the query body or a DocumentNode
(AST) created by graphql-tag
.
vue<template>
<div>
<ul v-if="data">
<li v-for="todo in data.todos">{{ todo.text }}</li>
</ul>
</div>
</template>
<script setup>
import { useQuery } from 'villus';
const GetTodos = `
GetTodos {
todos {
id
title
}
}
`;
const { data } = useQuery({
query: GetTodos,
});
</script>
With graphql-tag
You can use graphql-tag
to compile your queries or load them with the graphql-tag/loader
.
This is an example with the useQuery
function:
vue<script setup>
import { gql } from 'graphql-tag';
const GetTodos = gql`
GetTodos {
todos {
id
title
}
}
`;
const { data } = useQuery({
query: GetTodos,
});
</script>
If you are using webpack you can configure the loader graphql-tag/loader
and use import/require
:
vue<script setup>
import { Todos } from '@/graphql/todos.gql';
const { data } = useQuery({
query: Todos,
});
</script>
Query Variables
You can pass variables to your queries as the second argument of useQuery
:
vue<script setup>
const FetchTodo = `
query FetchTodo ($id: ID!) {
todo (id: $id) {
text
}
}
`;
const { data } = useQuery({
query: GetTodos,
variables: { id: 123 },
});
</script>
However, if you want to re-fetch the query whenever the variables change, then this is where the composition API shines. You can pass a reactive object containing your variables and the query will automatically execute with the new variables value:
vue<script setup>
import { reactive } from 'vue';
import { useQuery } from 'villus';
const variables = reactive({
id: 123,
});
const { data } = useQuery({
query: `query FetchTodo ($id: ID!) {
todo (id: $id) {
text
}
}
`,
variables,
});
</script>
This also works with reactive Refs
vue<script setup>
import { ref } from 'vue';
import { useQuery } from 'villus';
const variables = ref({
id: 123,
});
const FetchTodo = `
query FetchTodo ($id: ID!) {
todo (id: $id) {
text
}
}
`;
const { data } = useQuery({
query: FetchTodo,
variables,
});
</script>
This is only one way to re-fetch queries because villus
is built with composable API first you will find many ways to re-fetch your queries no matter how complex your requirements are.
Re-fetching Queries
Sometimes you want to re-fetch the query or run it after some action, the execute
function that is returned from the useQuery
function. When called it re-executes the query.
This example executes the query after the button has been clicked, note that the query is still fetched initially.
Here is a snippet for calling execute
with useQuery
:
vue<script>
import { useQuery } from 'villus';
const { data, execute } = useQuery({
// ...
});
// call execute whenever you want the query to re-fetch
execute();
</script>
This can be very useful in situations where you have complex logic that triggers a refetch, which means watch
and watchEffect
play well with the execute
function:
vue<script setup>
import { watch } from 'vue';
import { useQuery } from 'villus';
const GetTodos = `
GetTodos {
todos {
id
title
}
}
`;
const { data, execute } = useQuery({
query: GetTodos,
});
watch(someComputedProp, () => execute());
</script>
Reactive Queries
Vue is all about reactivity to achieve better DX, and villus follows this philosophy as well. You are not only limited to reactive variables, you can also have reactive queries. In other words, queries created with ref
or computed
are recognized as reactive queries and will be watched automatically and re-fetched whenever the query changes.
vue<script setup>
import { computed, ref } from 'vue';
import { useQuery } from 'villus';
// computed id that will be used to compute the query
const id = ref(1);
// Create a computed query
const FetchTodo = computed(() => {
return `query FetchTodo {
todo (id: ${id.value}) {
text
}
}
`;
});
const { data } = useQuery({
query: FetchTodo,
});
// later on, changing the `id` ref will automatically refetch the query because it is computed
id.value = 2;
</script>
Reactive queries are very flexible and one of the many advantages of using the composition API.
Disabling Re-fetching
You can disable the automatic refetch behavior by passing a paused
getter function to the useQuery
function. The getter should return a boolean.
vue<script setup>
import { ref } from 'vue';
import { useQuery } from 'villus';
const GetPostById = `
query getPost ($id: ID!) {
post (id: $id) {
id
title
}
}
`;
// Create a reactive variables object
const variables = ref({ id: 123 });
const { data } = useQuery({
query: GetPostById,
variables,
paused: () => !variables.id, // Don't re-fetch automatically unless the id is present
});
</script>
The previous example can be also re-written as shown below, since the paused
function receives the current variables as an argument.
vue<script setup>
import { useQuery } from 'villus';
const { data } = useQuery({
query: GetPostById,
variables,
// Don't re-fetch automatically unless the id is present
paused: ({ id }) => !id,
});
</script>
This is useful if you want to build variable guards to make sure you don’t pass invalid values to your GraphQL servers.
In addition to passing a function, you can also pass reactive refs or a plain boolean:
vue<script setup>
const { data } = useQuery({
query: GetPostById,
variables,
// computed or `ref`
paused: computed(() => !variables.id),
});
const { data, execute } = useQuery({
query: GetPostById,
variables,
// boolean, this query is now "lazy" and you have to trigger executions manually with `execute`.
paused: true,
});
function runQuery() {
// won't be stopped
execute();
}
</script>
Whenever the paused
is a reactive value and it changes to false
, the query will be re-executed automatically so you can also use paused
to wait for when some condition is met before the query is executed. For example, maybe you have a query that depends on another and would like to make sure not to fetch the second one unless the first one was fetched.
vue<script setup>
const Post = `
query GetPost ($postId: ID!) {
post (id: $postId) {
id
title
}
}
`;
const Comments = `
query Comments ($postId: ID!) {
post (id: $postId) {
comments {
body
}
}
}
`;
const variables = { postId: 1 };
const { data: postData } = useQuery({
query: Post,
variables,
});
const { data } = useQuery({
query: Comments,
variables,
// Causes the query to wait for the post to be found and fetched.
paused: () => !!postData.value.post,
});
</script>
Skipping Queries
You can also skip executing queries by providing a skip
argument to the query options. This can be particularly useful if you want to prevent fetching or refetching a query if a variable value is invalid. This may seem similar to paused
except it doesn’t stop query or variables watching and it prevents all executions, even manual ones with execute
. Also, it doesn’t re-fetch automatically whenever it is set back to false
.
In the following example, we skip the query unless the user has entered enough characters for the search terms.
vue<template>
<div>
<input v-model="searchTerm" type="search" placeholder="Enter search terms" />
<ul v-if="data">
<li v-for="post in data.searchPosts" :key="post.id">{{ post.title }}</li>
</ul>
<p v-if="isFetching">Searching...</p>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useQuery } from 'villus';
const SearchPosts = `
query SearchPosts($term: String!) {
searchPosts (term: $term) {
id
title
}
}
`;
const searchTerm = ref('');
// Skip the query if the search term has less than 3 characters.
const shouldSkip = computed(() => {
return searchTerm.value && searchTerm.value.length >= 3;
});
const { data, isFetching } = useQuery({
query: SearchPosts,
skip: shouldSkip,
variables: computed(() => {
return {
term: searchTerm.value,
};
}),
});
</script>
You can also pass a function instead of a reactive variable, this function receives the current variables as an argument.
vue<template>
<div>
<input v-model="searchTerm" type="search" placeholder="Enter search terms" />
<ul v-if="data">
<li v-for="post in data.searchPosts" :key="post.id">{{ post.title }}</li>
</ul>
<p v-if="isFetching">Searching...</p>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useQuery } from 'villus';
const SearchPosts = `
query SearchPosts($term: String!) {
searchPosts (term: $term) {
id
title
}
}
`;
const searchTerm = ref('');
const { data, isFetching } = useQuery({
query: SearchPosts,
skip: ({ term }) => {
return term && term.length >= 3;
},
variables: computed(() => {
return {
term: searchTerm.value,
};
}),
});
</script>
Of course, you could’ve used paused
for the previous example, but because paused
stops watching the query variables it means your query won’t trigger whenever the user type something into the search terms. Also, it wouldn’t work correctly if you call execute
manually with a watcher because paused
doesn’t stop manual executions. This makes skip
ideal for situations where you want to keep the reactivity of the query while also ignoring certain executions of it.
Fetching on Mounted
By default queries are executed when the component is mounted. You can configure this behavior by setting the fetchOnMount
option:
vue<script setup>
import { useQuery } from 'villus';
const GetPosts = `
query GetPosts {
posts {
id
title
}
}
`;
const { data } = useQuery({
query: GetPosts,
// disables query fetching on mounted
fetchOnMount: false,
});
</script>
Pausing and Skipping with fetchOnMount
Note that this behavior is subject to paused
or skip
being set. Meaning if a query is paused or skipped it won’t fetch when the component is mounted.
Caching
Queries are cached in memory, the uniqueness criteria is the query name, body, and its variables. Meaning if the same query is run with the same variables it will be fetched from the cache by default and will not hit the network. Cache is deleted after the user closes/refreshes the page.
By default the client uses cache-first
policy to handle queries, the full list of available policies are:
cache-first
: If found in cache return it, otherwise fetch it from the networknetwork-only
: Always fetch from the network and do not cache itcache-and-network
: If found in cache return it, then fetch a fresh result from the network and update current data (reactive). if not found in cache, it will fetch it from the network and cache itcache-only
: If found in cache return it, otherwise returnsnull
for bothdata
anderrors
You can specify a different strategy on different levels:
On the client level
You can set the default policy on the client level when you are building the GraphQL client by passing cachePolicy
option to either:
vue<script setup>
import { useClient } from 'villus';
useClient({
url: '/graphql', // Your endpoint
cachePolicy: 'network-only',
});
</script>
This will make all the child-components using useQuery
use the network-only
policy by default.
On the query level
You can pass the cachePolicy
property to the useQuery
function to set the default caching policy for that query:
tip
Note the usage of a different signature here for the useQuery
function, what you have seen so far is the “short-hand” but when you need to modify the query behavior you will need to use the full or extended options. The main difference is that this signature only accepts exactly 1 argument containing the query options, you can find more information about the available options in the API reference page.
vue<script setup>
import { useQuery } from 'villus';
const GetPosts = `
query GetPosts {
posts {
id
title
}
}
`;
const { data } = useQuery({
query: GetPosts,
cachePolicy: 'network-only',
});
</script>
On each execute
call level
You can also set the caching policy for a single execute
call by passing it to the execute
function provided by the slot props or the useQuery
function.
Here is a snippet for doing so with the useQuery
function:
vue<script setup>
import { useQuery } from 'villus';
const GetPosts = `
query GetPosts {
posts {
id
title
}
}
`;
const { execute, data } = useQuery({
query: GetPosts,
});
// use this in template or whatever.
function runWithPolicy() {
execute({ cachePolicy: 'network-only' });
}
</script>
You can build your own cache layer and plugins for villus, check the Plugins Guide
Query tags 2.1.0
You can tag the queries with an array of strings. These tags have a couple of uses:
- You can clear the cache for those tagged queries and also allow mutations to auto-clear the queries’ cache that has any of the same tags.
- You can refetch all of tagged queries after a mutation that has any of the same tags.
You can specify tags by passing tags
to the query options when first calling useQuery
composable:
vue<script setup>
import { useQuery } from 'villus';
const GetPosts = `
query GetPosts {
posts {
id
title
}
}
`;
const { execute, data } = useQuery({
query: GetPosts,
tags: ['all_posts'],
});
</script>
The previous snippet tags the query with all_posts
tag. Meaning any mutation that has clearCacheTags
with the same value will clear this query’s cache. So next time it is refetched, the query will be go to the network, skipping the cache entirely.
To see how mutations can be configured to clear tagged queries or refetch them, check the mutation’s guide.
You can manually clear the entire cache or any specific cache tags, read more in the cache plugin page.
Suspense
Vue 3
This feature is only available with Vue 3 at the moment
You can use useQuery
with the Suspense
API component shipped in Vue 3.x.
To utilize the suspense feature, you need to await
the useQuery
function and it returns the exact same API after executing the query:
vue<template>
<ul>
<li v-for="post in data.posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>
<script setup>
import { useQuery } from 'villus';
const GetPosts = `
query GetPosts {
posts {
id
title
}
}
`;
const { data } = await useQuery({
query: GetPosts,
});
</script>
Then you can suspend the Listing.vue
component like this:
vue<template>
<div>
<Suspense>
<template #default>
<Listing />
</template>
<template #fallback>
<span>Loading...</span>
</template>
</Suspense>
</div>
</template>
<script setup>
import Listing from '@/components/Listing.vue';
</script>
Fetching Indication
It is very common to display an indication for pending queries in your UI so your users know that something is being done, the useQuery
composition function exposes a isFetching
boolean ref that you can use to display such indicators.
vue<template>
<div>
<ul v-if="data">
<li v-for="post in data.posts" :key="post.id">{{ post.title }}</li>
</ul>
<p v-if="isFetching">Loading...</p>
</div>
</template>
<script setup>
import { useQuery } from 'villus';
const GetPosts = `
query GetPosts {
posts {
id
title
}
}
`;
const { data, isFetching } = useQuery({
query: GetPosts,
});
</script>
Whenever a re-fetch is triggered, or the query was executed again, the isFetching
property will update accordingly so you don’t have to keep it in sync, nor will you have to create your own boolean refs for indications.
Initial isFetching value
The default value for isFetching
is true
if fetchOnMount
is enabled, otherwise it will default to false
.
Event hooks
useQuery returns event hooks allowing you to execute code when a specific event occurs.
onData
This is called whenever a new result is available.
vue<script setup>
import { useQuery } from 'villus';
const { onData } = useQuery({
query: GetPostById,
variables,
});
onData(data => {
// Do something
console.log(data);
});
</script>
You can also register multiple callbacks, if you need to.
tsonData(data => {
// Do one thing
});
onData(data => {
// Do another
});
You can unregister any callback by calling the function returned from calling onData
.
vue<script setup>
import { useQuery } from 'villus';
const { onData } = useQuery({
query: GetPostById,
variables,
});
const stop = onData(data => {
// Do something
console.log(data);
});
// removes the callback and will no longer execute
stop();
</script>
useQuery
also accepts onData
option if you are only interested in registering one callback:
tsimport { useQuery } from 'villus';
useQuery({
query: GetPostById,
variables,
onData: data => console.log(data),
});
onError
only triggered when an error occurs.
vue<script setup>
import { useQuery } from 'villus';
const { onError } = useQuery({
query: GetPostById,
variables,
});
onError(error => {
// Handle the error
});
</script>
You can also register multiple callbacks, if you need to.
tsonError(error => {
// Do one thing
});
onError(error => {
// Do another
});
You can unregister any callback by calling the function returned from calling onError
.
vue<script setup>
import { useQuery } from 'villus';
const { onError } = useQuery({
query: GetPostById,
variables,
});
const stop = onError(error => {
// Do something
});
// removes the callback and will no longer execute
stop();
</script>
useQuery
also accepts onError
option if you are only interested in registering one callback:
vue<script setup>
import { useQuery } from 'villus';
useQuery({
query: GetPostById,
variables,
onError: err => {
console.log(err);
},
});
</script>