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';23 // Incorrect4 const Todos = () => {5 const TODOS_QUERY = gql`6 {7 todos {8 title9 }10 }11 `;1213 const {data, isLoading, error} = useQuery(TODOS_QUERY);1415 // ...16};1718// Correct19const TODOS_QUERY = gql`20 {21 todos {22 title23 }24 }25`;2627const Todos = () => {28 const {data, isLoading, error} = useQuery(TODOS_QUERY);2930 // ...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 id5 title6 }7 }8 `;910const Todos = () => {11 const {data, isLoading, error} = useQuery(TODOS_QUERY);1213 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 id5 title6 }7 }8 `;910const Todos = () => {11 const {data, isLoading, error} = useQuery(TODOS_QUERY, {12 page: 113 });1415 // ...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});45 const onNextPage = () => {6 setPage(page => page + 1);7 };89 return (10 <>11 {/* ... */}1213 <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 queries2 const client = new Draqula('https://my-graphql-server.com/graphql', {3 cache: false4 });56 // Turn off caching for an individual query7 const {data, isLoading, error} = useQuery(8 TODOS_QUERY,9 {10 page: 111 },12 {13 cache: false14 }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);34 return (5 <>6 {/* ... */}78 <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.
Pagination
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 id6 title7 }8 pagination {9 hasMore10 nextPage11 }12 }13 }14`;1516const Todos = () => {17 const {data, isLoading, error, fetchMore, isFetchingMore} = useQuery(TODOS_QUERY, {18 // We don't need a dynamic `page` variable here, because19 // we're always starting with a first page initially, so a fixed initial value will work20 page: 121 });2223 const onFetchMore = () => {24 fetchMore({page: data.todos.pagination.nextPage});25 };2627 return (28 <>29 {isLoading && <span>Loading…</span>}30 {error && <span>Error: {error.message}</span>}3132 {data && (33 <ul>34 {data.todos.data.map(todo => (35 <li key={todo.id}>{todo.title}</li>36 ))}37 </ul>38 )}3940 {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';23 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 strategy8 return;9 }1011 // Append new items to the end of the existing array12 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 array4 // ...5 // return nextValue.concat(prevValue)6 // ...7 };89 const Todos = () => {10 const {data, isLoading, error, fetchMore} = useQuery(TODOS_QUERY);1112 const onFetchMore = () => {13 fetchMore(14 {15 page: data.todos.pagination.nextPage16 },17 {18 merge: customMerge19 }20 );21 };2223 // ...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 queries2 const client = new Draqula('https://graph.ql', {3 retry: false4 });56 // Turn off retries only for one query7 const {data, isLoading, error} = useQuery(8 TODOS_QUERY,9 {10 page: 111 },12 {13 retry: false14 }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: 54});
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';34 const IndexPage = () => {5 const client = useDraqulaClient();67 // Preload TODOS_QUERY once this component mounts8 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 title5 }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.