Queries

Queries in Draqula are executed via the useQuery React hook, exposed from the draqula module:

1import {useQuery} from 'draqula';

All queries must be using the gql tag function from the graphql-tag module and they must be defined outside your component. If you define a query inside your component, it will be reconstructed on every render, which will make Draqula refetch it infinitely.

1 import gql from 'graphql-tag';
2
3 // Incorrect
4 const Todos = () => {
5 const TODOS_QUERY = gql`
6 {
7 todos {
8 title
9 }
10 }
11 `;
12
13 const {data, isLoading, error} = useQuery(TODOS_QUERY);
14
15 // ...
16};
17
18// Correct
19const TODOS_QUERY = gql`
20 {
21 todos {
22 title
23 }
24 }
25`;
26
27const Todos = () => {
28 const {data, isLoading, error} = useQuery(TODOS_QUERY);
29
30 // ...
31};

Basic queries

Here's how to perform a simple query without any variables or options:

1 const TODOS_QUERY = gql`
2 {
3 todos {
4 id
5 title
6 }
7 }
8 `;
9
10const Todos = () => {
11 const {data, isLoading, error} = useQuery(TODOS_QUERY);
12
13 return (
14 <>
15 {isLoading && <span>Loading…</span>}
16 {error && <span>Error: {error.message}</span>}
17 {data && (
18 <ul>
19 {data.todos.map(todo => (
20 <li key={todo.id}>{todo.title}</li>
21 ))}
22 </ul>
23 )}
24 </>
25 );
26};

useQuery returns an object with these essential fields:

  • data - Query response
  • isLoading - Boolean that indicates if the query is still loading
  • error - Network or GraphQL error

We're not going to take a deep dive into error handling yet, but you can jump into the "Error Handling" page to learn more about it. For the sake of simplicity of the following examples, I'm going to skip all JSX code going forward.

Variables

Variables can be passed to a query via a second argument to useQuery, like so:

1 const TODOS_QUERY = gql`
2 query Todos($page: Int!) {
3 todos(page: $page) {
4 id
5 title
6 }
7 }
8 `;
9
10const Todos = () => {
11 const {data, isLoading, error} = useQuery(TODOS_QUERY, {
12 page: 1
13 });
14
15 // ...
16};

Whenever query variables are updated, Draqula will fetch the same query with new variables as well as reset data to undefined and mark the query as loading (loading === true).

1 const Todos = () => {
2 const [page, setPage] = useState(1);
3 const {data, isLoading, error} = useQuery(TODOS_QUERY, {page});
4
5 const onNextPage = () => {
6 setPage(page => page + 1);
7 };
8
9 return (
10 <>
11 {/* ... */}
12
13 <button onClick={onNextPage}>Load next page</button>
14 </>
15 );
16};

If a user loaded this page, they would see a loading indicator first and eventually a list of todos. After they clicked the "Load next page" button, the list would be erased and replaced with a loading indicator again. At that time, Draqula would start fetching a query with a new page variable.

Caching

Draqula caches data based on the query and the variables you pass to it. For example, if you loaded TODOS_QUERY with page = 1 before, next time you attempt to request the same query with the same page = 1 variable, Draqula will immediately return the last data stored for that query. However, it will still send a request to refetch that data, but in background. So in cases when there's a cache for a query, isLoading will equal to false, even when the query is refetching in the background.

It's important to mention, that Draqula implements basic cache mechanism and it invalidates data very aggressively. If a mutation returns any of the types used in a query, that query's cache will be invalidated and if a query is currently rendered on the page, it will be refetched.

Unlike other GraphQL clients, Draqula also doesn't have normalization of data by id or other fields. Lack of this functionality eliminates a whole range of issues, like incorrect data merges, failed cache reads/writes or requirement to manually specify queries to refetch after a mutation.

Caching can be turned off either globally or individually for each query:

1 // Turn off caching for all queries
2 const client = new Draqula('https://my-graphql-server.com/graphql', {
3 cache: false
4 });
5
6 // Turn off caching for an individual query
7 const {data, isLoading, error} = useQuery(
8 TODOS_QUERY,
9 {
10 page: 1
11 },
12 {
13 cache: false
14 }
15);

Refetch

Sometimes query results can be out-dated and it's necessary to manually refetch the query due to some side effect. This is possible via refetch function that's returned from useQuery:

1 const Todos = () => {
2 const {data, isLoading, error, refetch} = useQuery(TODOS_QUERY);
3
4 return (
5 <>
6 {/* ... */}
7
8 <button onClick={refetch}>Refresh data</button>
9 </>
10 );
11};

Note that refetching a query doesn't reset its current data and will not switch isLoading to true, so there won't be a loading indicator. It's implemented this way, so that user still sees data on the screen, even if fresh data is coming.

Most often, you won't need to call refetch manually, since Draqula is very aggressive about cache invalidation. You can learn more about Draqula's caching mechanism in the "Caching" section above.

If all you need is page-based pagination, where the previous set of data is replaced with a new one, this section is not applicable to your use-case. For that, you can simply increment the page variable and pass it to your query via variables.

However, if your app needs to add more data to the list along with existing one (for example, infinite loading), meet the fetchMore function. As the name implies, fetchMore fetches more data without deleting the current data. Let's use page-based pagination for simplicity, but assume that we want to append new todo items at the end of the list.

1 const TODOS_QUERY = gql`
2 query Todos($page: Int!) {
3 todos(page: $page) {
4 data {
5 id
6 title
7 }
8 pagination {
9 hasMore
10 nextPage
11 }
12 }
13 }
14`;
15
16const Todos = () => {
17 const {data, isLoading, error, fetchMore, isFetchingMore} = useQuery(TODOS_QUERY, {
18 // We don't need a dynamic `page` variable here, because
19 // we're always starting with a first page initially, so a fixed initial value will work
20 page: 1
21 });
22
23 const onFetchMore = () => {
24 fetchMore({page: data.todos.pagination.nextPage});
25 };
26
27 return (
28 <>
29 {isLoading && <span>Loading…</span>}
30 {error && <span>Error: {error.message}</span>}
31
32 {data && (
33 <ul>
34 {data.todos.data.map(todo => (
35 <li key={todo.id}>{todo.title}</li>
36 ))}
37 </ul>
38 )}
39
40 {data.todos.pagination.hasMore && (
41 <button disabled={isFetchingMore} onClick={onFetchMore}>
42 {isFetchingMore ? 'Loading more todos…' : 'Load more todos'}
43 </button>
44 )}
45 </>
46 );
47};

To prevent you from managing the loading state of fetchMore manually (since isLoading will remain false when fetchMore is called), Draqula exposes a isFetchingMore variable. It's the same as isLoading, but only reflects the state of the fetchMore operation.

Merging data

Draqula uses the _.merge function from Lodash to merge new data into an existing one with custom behavior for arrays. When Draqula encounters an array in a new response, it will append all the items of that array to the same array in the existing data. If the array in a new response is null, Draqula will ignore it.

This is how merging is implemented in Draqula by default:

1 import {cloneDeep, mergeWith} from 'lodash';
2
3 export default (prevData, nextData) => {
4 return cloneDeep(
5 mergeWith(prevData, nextData, (prevValue, nextValue) => {
6 if (!Array.isArray(prevValue)) {
7 // Returning nothing tells Lodash to use default merging strategy
8 return;
9 }
10
11 // Append new items to the end of the existing array
12 return prevValue.concat(nextValue);
13 })
14 );
15};

If you want to use a custom merge strategy, for example, to insert items into the start of an array, you can pass a custom merge function to fetchMore:

1 const customMerge = (oldData, newData) => {
2 // You could copy the default implementation above,
3 // but insert items into the start of an array
4 // ...
5 // return nextValue.concat(prevValue)
6 // ...
7 };
8
9 const Todos = () => {
10 const {data, isLoading, error, fetchMore} = useQuery(TODOS_QUERY);
11
12 const onFetchMore = () => {
13 fetchMore(
14 {
15 page: data.todos.pagination.nextPage
16 },
17 {
18 merge: customMerge
19 }
20 );
21 };
22
23 // ...
24};

Retries

Draqula automatically retries all queries by default, if these conditions are true:

  • There were less than 2 retries (this is configurable)
  • The request didn't respond with 4xx HTTP status code

Retries can be turned off globally or for each query individually:

1 // Turn off retries for all queries
2 const client = new Draqula('https://graph.ql', {
3 retry: false
4 });
5
6 // Turn off retries only for one query
7 const {data, isLoading, error} = useQuery(
8 TODOS_QUERY,
9 {
10 page: 1
11 },
12 {
13 retry: false
14 }
15);

It's also possible to configure how many times Draqula should retry the query:

1// Retry 5 times (there will be 6 requests since the initial request doesn't count)
2const client = new Draqula('https://graph.ql', {
3 retry: 5
4});

If you want more granular control over retries, you can pass an object of options. These options are the same as the ones accepted by retry module.

Preloading

Preloading queries is useful for speeding up user experience, when you know which page user is most likely to open next.

To do so, we can use preload method exposed by the client:

1 import {useEffect} from 'react';
2 import {useDraqulaClient} from 'draqula';
3
4 const IndexPage = () => {
5 const client = useDraqulaClient();
6
7 // Preload TODOS_QUERY once this component mounts
8 useEffect(() => {
9 client.preload(TODOS_QUERY, {page: 0});
10 }, []);
11};

Next time other component tries to load TODOS_QUERY, cached data will be returned immediately.

API

useQuery(query, variables?, options?)

This hook returns QueryResult object.

query

Type: DocumentNode

Parsed GraphQL query via gql function from graphql-tag module. For example:

1const TODOS_QUERY = gql`
2 {
3 todos {
4 title
5 }
6 }
7`;

variables

Type: object
Default: {}

Variables to pass along with query.

options

Type: object

options.cache

Type: boolean
Default: true

Determines whether to use caching for this query or not. This option overrides the global cache option in the client itself.

options.retry

Type: boolean
Default: true

Determines whether to retry this query or not.

options.timeout

Type: number
Default: 10000

Timeout to use for this query in milliseconds. This value overrides the timeout specified in the client itself.

options.refetchOnFocus

Type: boolean
Default: true

Refetch when window gets focused. Useful for keeping data on your page up-to-date when user comes back.

QueryResult

useQuery hook returns an object consisting of the following fields.

data

Type: object | undefined

Data from the query response.

isLoading

Type: boolean

Indicates whether the query is currently loading or not. isLoading is still false if query is being refetched via refetch() or fetchMore().

error

Type: NetworkError | GraphQLError | undefined

Returns an error that occurred during the request, if there was one. Otherwise returns undefined. Read more in the "Error handling" section.

fetchMore(variables, fetchMoreOptions)

Type: Function

Fetches more data for the current query and merges it with the existing data, instead of replacing it. Commonly used for pagination. Variables for this request can be specified via variables object as the first argument. These variables will overwrite the original variables passed to useQuery.

Custom merge function can be passed via merge field in fetchMoreOptions object as the second argument:

1fetchMore({page: 2}, {merge: customMergeFn});

isFetchingMore

Type: boolean
Default: false

Indicates whether the fetchMore operation is currently in-flight.

refetch()

Type: Function

Refetches the query with the same variables. Useful for manually refreshing data.