Hi,
I've setup react-query for my fetching and caching recently. I really like it, it suppresses completely the need for a state store for parts of the app that are just read/write the API without much business logic. I now have a nice separation of where data lies depending on whether I'm just read-writing it, or manipulating it in complex ways. Particularly love the refetch on mutation pattern. And sending some love for the suspense mode of course. Thanks for the great lib!
Now, to the topic I wanted to discuss. My API is using GraphQL and I have created a custom hook that wraps react-query to incorporate all the repetitive code I would do in each component.
I don't have any burning question, just wanted to share, get some feedback, have some conversation about this approach. I hope it can help me and others. Maybe there are things in here that can be generalized and reused whether in react-query or another small wrapper lib.
Oh, and please let me know if there is a better place to discuss this!
Below is my implementation.
// gql.js - my custom hook
import { useEffect } from 'react'
import { useQuery, useMutation } from 'react-query'
import { GraphQLClient } from 'graphql-request'
import { print } from 'graphql/language/printer'
import { useAuth0 } from '../contexts/Auth0'
const getClient = token =>
new GraphQLClient(process.env.REACT_APP_GQL_ENDPOINT, {
headers: {
Authorization: `Bearer ${token}`
}
})
export const getQueryKey = query => query.definitions[0].name.value
// qvFn stands for query & variables function
export const useGqlQuery = qvFn => {
const { getTokenSilently } = useAuth0()
return useQuery(
() => {
const [query, variables] = qvFn()
return [getQueryKey(query), variables]
},
async variables => {
const token = await getTokenSilently()
return getClient(token).request(print(qvFn()[0]), variables)
}
)
}
export const useGqlMutation = (query, options = {}) => {
const { getTokenSilently } = useAuth0()
const [mutate, { data, isLoading, error }] = useMutation(
async variables => {
const token = await getTokenSilently()
return getClient(token).request(print(query), variables)
},
{
refetchQueries: (options.refetchQueries || []).map(getQueryKey),
refetchQueriesOnFailure: (options.refetchQueriesOnFailure || []).map(getQueryKey)
}
)
const callOnData = () => {
if (data && options.onData) {
options.onData(data)
}
}
useEffect(callOnData, [data])
const callOnError = () => {
if (error && options.onError) {
options.onError(error)
}
}
useEffect(callOnError, [error])
return [mutate, { data, isLoading, error }]
}
and the querying component looks like this:
// component.js - a random component
// imports [...]
import gql from 'graphql-tag'
import { useGqlQuery, useGqlMutation } from '../hooks/gql'
export const fetchData = gql`[...]`
const updateData = gql`[...]`
const MyComponent = () => {
const { data, isFetching } = useGqlQuery(() => [fetchData, { /* vars [...] */ }])
const [mutate, { isLoading }] = useGqlMutation(updateData, {
refetchQueries: [fetchData],
onData: () => setSuccess('<success message>'), // set some state
onError: () => setError('<error message>') // set some state
})
// [...] state / event handlers / render method
}
export default MyComponent
Note: I'm only using the suspense mode.
What does this adds to the normal hooks:
- custom fetch method based on graphql-request
- graphql queries are passed in AST format
- queries are "printed" just to fetch (because graphql-request do not consume gql AST objects)
- gql is used to generate the AST (and enables linting with the good editor extensions)
- generate unique key based on query AST
- unique key is also generated from the same query object when prefetching or refetching
- i'm keeping the query function and throw behaviour (I love it)
- auth token is injected
- mutations have
onData
and onError
options that are functions I can run when the query resolves or fails, this saves on a bunch of useEffect
calls in the component.
I like graphql-request because it's minimal, instead of going with something like apollo-client, we can separate concerns of fetching with a dedicated tiny lib (graphql-request), and caching + suspense with another tiny lib (react-query).
Some of these would be feasible with just a custom fetch method. Not key generation, and it seems nice to just pass a query AST and have the key be deterministic. Like this, it's impossible to end up with non-matching keys. What do you think of this? I could also hash the whole query body instead of just take the name, it would be more safe. It's probably something @tannerlinsley has thought about in choosing string keys instead of hashing queries.
Also onData
and onError
are probably hard to accomplish without a custom hook wrapper, and I feel it's extra nice. Maybe something worth adding to the lib?
Are there any pitfalls I have missed here? Or anything nifty I could add? Do you think that's a good approach? I'm curious what the community will think about this.