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 network
  • network-only: Always fetch from the network and do not cache it
  • cache-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 it
  • cache-only: If found in cache return it, otherwise returns null for both data and errors

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>