graphql-hooks
graphql-hooks copied to clipboard
π£ Minimal hooks-first GraphQL client
graphql-hooks
π£ Minimal hooks-first GraphQL client.
Features
- π₯ First-class hooks API
- βοΈ Tiny bundle: only 7.6kB (2.8 gzipped)
- π Full SSR support: see graphql-hooks-ssr
- π Plugin Caching: see graphql-hooks-memcache
- π₯ No more render props hell
- β³ Handle loading and error states with ease
Install
npm install graphql-hooks
or
yarn add graphql-hooks
Support
- Node LTS
- Browsers
> 1%, not dead
Consider polyfilling:
-
FormData
-
Promise
-
fetch
. NOTE: A custom implementation can also be provided instead of polyfilling, seeGraphQLClient
Quick Start
First you'll need to create a client and wrap your app with the provider:
import { GraphQLClient, ClientContext } from 'graphql-hooks'
const client = new GraphQLClient({
url: '/graphql'
})
function App() {
return (
<ClientContext.Provider value={client}>
{/* children */}
</ClientContext.Provider>
)
}
Now in your child components you can make use of useQuery
import { useQuery } from 'graphql-hooks'
const HOMEPAGE_QUERY = `query HomePage($limit: Int) {
users(limit: $limit) {
id
name
}
}`
function MyComponent() {
const { loading, error, data } = useQuery(HOMEPAGE_QUERY, {
variables: {
limit: 10
}
})
if (loading) return 'Loading...'
if (error) return 'Something Bad Happened'
return (
<ul>
{data.users.map(({ id, name }) => (
<li key={id}>{name}</li>
))}
</ul>
)
}
Why graphql-hooks
?
The first thing you may ask when seeing graphql-hooks
is "Why not use Apollo hooks?".
It's the comparison most will make. In fact, there's an article comparing the two over on LogRocket.
We believe graphql-hooks
is a great choice as a hooks-first GraphQL client due to its concise API and package size.
In terms of performance, this is more of a grey area as we have no official benchmarks yet.
If you need a client that offers more customization such as advanced cache configuration, then apollo-hooks
may work out to be a good choice for your project if bundle size is not an issue.
Pros | Cons |
---|---|
Small in size | Less "advanced" caching configuration |
Concise API | |
Quick to get up and running |
Table of Contents
- API
- GraphQLClient
- ClientContext
- useQuery
- useManualQuery
- useMutation
- useSubscription
- Guides
- SSR
- Pagination
- File uploads
- HTTP Get Support
- Authentication
- Fragments
-
Migrating from Apollo
- ApolloClient β‘οΈ GraphQLClient
- ApolloProvider β‘οΈ ClientContext.Provider
- Query Component β‘οΈ useQuery
- Mutation Component β‘οΈ useMutation
- Testing and mocking
- [Other]
- Request interceptors
API
GraphQLClient
Usage:
import { GraphQLClient } from 'graphql-hooks'
const client = new GraphQLClient(config)
config
: Object containing configuration properties
-
url
: The URL of your GraphQL HTTP server. If not specified, you must enablefullWsTransport
and provide a validsubscriptionClient
; otherwise is required. -
fullWsTransport
: Boolean - set totrue
if you want to usesubscriptionClient
to also send query and mutations via WebSocket; defaults tofalse
-
ssrMode
: Boolean - set totrue
when using on the server for server-side rendering; defaults tofalse
-
useGETForQueries
: Boolean - set totrue
to use HTTP GET method for all queries; defaults to false. See HTTP Get Support for more info -
subscriptionClient
: The WebSocket client configuration. Accepts either an instance ofSubscriptionClient
from subscriptions-transport-ws orClient
from graphql-ws. A factory function is also accepted e.g. to avoid the creation of the client in SSR environments. -
cache
(Required ifssrMode
istrue
, otherwise optional): Object with the following methods:-
cache.get(key)
-
cache.set(key, data)
-
cache.delete(key)
-
cache.clear()
-
cache.keys()
-
getInitialState()
- See graphql-hooks-memcache as a reference implementation
-
-
fetch(url, options)
: Fetch implementation - defaults to the globalfetch
API. Check Request interceptors for more details how to managefetch
. -
FormData
: FormData implementation - defaults to the globalFormData
API. Polyfill this in a node.js environment. See file-uploads-nodejs for more info. -
fetchOptions
: See MDN for info on what options can be passed -
headers
: Object, e.g.{ 'My-Header': 'hello' }
-
logErrors
: Boolean - defaults totrue
-
middleware
: Accepts an array of middleware functions, default: none, see more in middlewares readme -
onError({ operation, result })
: Custom error handler-
operation
: Object withquery
,variables
andoperationName
-
result
: Object containingdata
anderror
object that containsfetchError
,httpError
andgraphqlErrors
-
client
methods
-
client.setHeader(key, value)
: Updatesclient.headers
adding the new header to the existing headers -
client.setHeaders(headers)
: Replacesclient.headers
-
client.removeHeader(key)
: Updatesclient.headers
removing the header if it exists -
client.logErrorResult({ operation, result })
: Default error logger; useful if you'd like to use it inside your customonError
handler -
request(operation, options)
: Make a request to your GraphQL server; returning a Promise-
operation
: Object withquery
,variables
andoperationName
-
-
options.fetchOptionsOverrides
: Object containing additional fetch options to be added to the default ones passed tonew GraphQLClient(config)
-
options.responseReducer
: Reducer function to pick values from the original Fetch Response object. Values are merged to therequest
response under thedata
key. Example usage:{responseReducer: (data, response) => ({...data, myKey: response.headers.get('content-length)})
ClientContext
ClientContext
is the result of React.createContext()
- meaning it can be used directly with React's new context API:
Example:
import { ClientContext } from 'graphql-hooks'
function App() {
return (
<ClientContext.Provider value={client}>
{/* children can now consume the client context */}
</ClientContext.Provider>
)
}
To access the GraphQLClient
instance, call React.useContext(ClientContext)
:
import React, { useContext } from 'react'
import { ClientContext } from 'graphql-hooks'
function MyComponent() {
const client = useContext(ClientContext)
}
useQuery
Usage:
const state = useQuery(query, [options])
Example:
import { useQuery } from 'graphql-hooks'
function MyComponent() {
const { loading, error, data } = useQuery(query)
if (loading) return 'Loading...'
if (error) return 'Something bad happened'
return <div>{data.thing}</div>
}
This is a custom hook that takes care of fetching your query and storing the result in the cache. It won't refetch the query unless query
or options.variables
changes.
-
query
: Your GraphQL query as a plain string -
options
: Object with the following optional properties-
variables
: Object e.g.{ limit: 10 }
-
operationName
: If your query has multiple operations, pass the name of the operation you wish to execute. -
persisted
: Boolean - defaults tofalse
; Passtrue
if your graphql server supportspersisted
flag to serve persisted queries. -
useCache
: Boolean - defaults totrue
; cache the query result -
skip
: Boolean - defaults tofalse
; do not execute the query if set totrue
-
skipCache
: Boolean - defaults tofalse
; Iftrue
it will by-pass the cache and fetch, but the result will then be cached for subsequent calls. Note therefetch
function will do this automatically -
ssr
: Boolean - defaults totrue
. Set tofalse
if you wish to skip this query during SSR -
fetchOptionsOverrides
: Object - Specific overrides for this query. See MDN for info on what options can be passed -
updateData(previousData, data)
: Function - Custom handler for merging previous & new query results; return value will replacedata
inuseQuery
return value-
previousData
: Previous GraphQL query orupdateData
result -
data
: New GraphQL query result
-
-
client
: GraphQLClient - If a GraphQLClient is explicitly passed as an option, then it will be used instead of the client from theClientContext
. -
refetchAfterMutations
: String | Object | (String | Object)[] - You can specify when a mutation should trigger query refetch.- If it's a string, it's the mutation string
- If it's an object then it has properties mutation and filter
-
mutation
: String - The mutation string -
filter
: Function (optional) - It receives mutation's variables as parameter and blocks refetch if it returns false
-
- If it's an array, the elements can be of either type above
-
useQuery
return value
const { loading, error, data, refetch, cacheHit } = useQuery(QUERY)
-
loading
: Boolean -true
if the query is in flight -
data
: Object - the result of your GraphQL query -
refetch(options)
: Function - useful when refetching the same query after a mutation; NOTE this presetsskipCache=true
& will bypass theoptions.updateData
function that was passed intouseQuery
. You can pass a newupdateData
intorefetch
if necessary.-
options
: Object - options that will be merged into theoptions
that were passed intouseQuery
(see above).
-
-
cacheHit
: Boolean -true
if the query result came from the cache, useful for debugging -
error
: Object - Set if at least one of the following errors has occurred and contains:-
fetchError
: Object - Set if an error occurred during thefetch
call -
httpError
: Object - Set if an error response was returned from the server -
graphQLErrors
: Array - Populated if any errors occurred whilst resolving the query
-
useManualQuery
Use this when you don't want a query to automatically be fetched, or wish to call a query programmatically.
Usage:
const [queryFn, state] = useManualQuery(query, [options])
Example:
import { useManualQuery } from 'graphql-hooks'
function MyComponent(props) {
const [fetchUser, { loading, error, data }] = useManualQuery(GET_USER_QUERY, {
variables: { id: props.userId }
})
return (
<div>
<button onClick={fetchUser}>Get User!</button>
{error && <div>Failed to fetch user<div>}
{loading && <div>Loading...</div>}
{data && <div>Hello ${data.user.name}</div>}
</div>
)
}
If you don't know certain options when declaring the useManualQuery
you can also pass the same options to the query function itself when calling it:
import { useManualQuery } from 'graphql-hooks'
function MyComponent(props) {
const [fetchUser] = useManualQuery(GET_USER_QUERY)
const fetchUserThenSomething = async () => {
const user = await fetchUser({
variables: { id: props.userId }
})
return somethingElse()
}
return (
<div>
<button onClick={fetchUserThenSomething}>Get User!</button>
</div>
)
}
useMutation
Mutations unlike Queries are not cached.
Usage:
const [mutationFn, state, resetFn] = useMutation(mutation, [options])
Example:
import { useMutation } from 'graphql-hooks'
const UPDATE_USER_MUTATION = `mutation UpdateUser(id: String!, name: String!) {
updateUser(id: $id, name: $name) {
name
}
}`
function MyComponent({ id, name }) {
const [updateUser] = useMutation(UPDATE_USER_MUTATION)
const [newName, setNewName] = useState(name)
return (
<div>
<input
type="text"
value={newName}
onChange={e => setNewName(e.target.value)}
/>
<button
onClick={() => updateUser({ variables: { id, name: newName } })}
/>
</div>
)
}
The options
object that can be passed either to useMutation(mutation, options)
or mutationFn(options)
can be set with the following properties:
-
variables
: Object e.g.{ limit: 10 }
-
operationName
: If your query has multiple operations, pass the name of the operation you wish to execute. -
fetchOptionsOverrides
: Object - Specific overrides for this query. See MDN for info on what options can be passed -
client
: GraphQLClient - If a GraphQLClient is explicitly passed as an option, then it will be used instead of the client from theClientContext
.
In addition, there is an option to reset the current state before calling the mutation again, by calling resetFn(desiredState)
where desiredState
is optional and if passed, it will override the initial state with:
-
data
: Object - the data -
error
: Error - the error -
loading
: Boolean - true if it is still loading -
cacheHit
: Boolean - true if the result was cached
useSubscription
To use subscription you can use either subscriptions-transport-ws or graphql-ws
API
useSubscription(operation, callback)
-
operation
: Object - The GraphQL operation the following properties:-
query
: String (required) - the GraphQL query -
variables
: Object (optional) - Any variables the query might need -
operationName
: String (optional) - If your query has multiple operations, you can choose which operation you want to call. -
client
: GraphQLClient - If a GraphQLClient is explicitly passed as an option, then it will be used instead of the client from theClientContext
.
-
-
callback
: Function - This will be invoked when the subscription receives an event from your GraphQL server - it will receive an object with the typical GraphQL response of{ data: <your result>, errors?: [Error] }
Usage:
First follow the quick start guide to create the client and povider. Then we need to update the config for our GraphQLClient
passing in the subscriptionClient
:
import { GraphQLClient } from 'graphql-hooks'
import { SubscriptionClient } from 'subscriptions-transport-ws'
// or
import { createClient } from 'graphql-ws'
const client = new GraphQLClient({
url: 'http://localhost:8000/graphql',
subscriptionClient: () =>
new SubscriptionClient('ws://localhost:8000/graphql', {
/* additional config options */
}),
// or
subscriptionClient: () =>
createClient({
url: 'ws://localhost:8000/graphql'
/* additional config options */
})
})
Next, within our React app, we can now make use of the useSubscription
hook.
import React, { useState } from 'react'
import { useSubscription } from 'graphql-hooks'
const TOTAL_COUNT_SUBSCRIPTION = `
subscription TotalCount {
totalCount {
count
}
}
`
function TotalCountComponent() {
const [count, setCount] = useState(0)
const [error, setError] = useState(null)
useSubscription({ query: TOTAL_COUNT_SUBSCRIPTION }, ({ data, errors }) => {
if (errors && errors.length > 0) {
// handle your errors
setError(errors[0])
return
}
// all good, handle the gql result
setCount(data.totalCount.count)
})
if (error) {
return <span>An error occurred {error.message}</span>
}
return <div>Current count: {count}</div>
}
Working Example:
See our subscription example which has both the client and server code to integrate subscriptions into your application.
See also the full WS transport example if you want to see how to send every operation through WebSocket.
Guides
SSR
See graphql-hooks-ssr for an in depth guide.
Pagination
GraphQL Pagination can be implemented in various ways and it's down to the consumer to decide how to deal with the resulting data from paginated queries. Take the following query as an example of offset pagination:
export const allPostsQuery = `
query allPosts($first: Int!, $skip: Int!) {
allPosts(first: $first, skip: $skip) {
id
title
url
}
_allPostsMeta {
count
}
}
`
In this query, the $first
variable is used to limit the number of posts that are returned and the $skip
variable is used to determine the offset at which to start. We can use these variables to break up large payloads into smaller chunks, or "pages". We could then choose to display these chunks as distinct pages to the user, or use an infinite loading approach and append each new chunk to the existing list of posts.
Separate pages
Here is an example where we display the paginated queries on separate pages:
import { React, useState } from 'react'
import { useQuery } from 'graphql-hooks'
export default function PostList() {
// set a default offset of 0 to load the first page
const [skipCount, setSkipCount] = useState(0)
const { loading, error, data } = useQuery(allPostsQuery, {
variables: { skip: skipCount, first: 10 }
})
if (error) return <div>There was an error!</div>
if (loading && !data) return <div>Loading</div>
const { allPosts, _allPostsMeta } = data
const areMorePosts = allPosts.length < _allPostsMeta.count
return (
<section>
<ul>
{allPosts.map(post => (
<li key={post.id}>
<a href={post.url}>{post.title}</a>
</li>
))}
</ul>
<button
// reduce the offset by 10 to fetch the previous page
onClick={() => setSkipCount(skipCount - 10)}
disabled={skipCount === 0}
>
Previous page
</button>
<button
// increase the offset by 10 to fetch the next page
onClick={() => setSkipCount(skipCount + 10)}
disabled={!areMorePosts}
>
Next page
</button>
</section>
)
}
Infinite loading
Here is an example where we append each paginated query to the bottom of the current list:
import { React, useState } from 'react'
import { useQuery } from 'graphql-hooks'
// use options.updateData to append the new page of posts to our current list of posts
const updateData = (prevData, data) => ({
...data,
allPosts: [...prevData.allPosts, ...data.allPosts]
})
export default function PostList() {
const [skipCount, setSkipCount] = useState(0)
const { loading, error, data } = useQuery(allPostsQuery, {
variables: { skip: skipCount, first: 10 },
updateData
})
if (error) return <div>There was an error!</div>
if (loading && !data) return <div>Loading</div>
const { allPosts, _allPostsMeta } = data
const areMorePosts = allPosts.length < _allPostsMeta.count
return (
<section>
<ul>
{allPosts.map(post => (
<li key={post.id}>
<a href={post.url}>{post.title}</a>
</li>
))}
</ul>
{areMorePosts && (
<button
// set the offset to the current number of posts to fetch the next page
onClick={() => setSkipCount(allPosts.length)}
>
Show more
</button>
)}
</section>
)
}
Refetch queries with mutations subscription
We can have a query to automatically refetch when any mutation from a provided list execute. In the following example we are refetching a list of posts for a given user.
Example
export const allPostsByUserIdQuery = `
query allPosts($userId: Int!) {
allPosts(userId: $userId) {
id
title
url
}
}
`
export const createPostMutation = `
mutation createPost($userId: Int!, $text: String!) {
createPost(userId: $userId, text: $text) {
id
title
url
}
}
`
const myUserId = 5
useQuery(allPostsByUserIdQuery, {
variables: {
userId: myUserId
},
refetchAfterMutations: [
{
mutation: createPostMutation,
filter: variables => variables.userId === myUserId
}
]
})
File uploads
graphql-hooks
complies with the GraphQL multipart request spec, allowing files to be used as query or mutation arguments. The same spec is also supported by popular GraphQL servers, including Apollo Server (see list of supported servers here).
If there are files to upload, the request's body will be a FormData
instance conforming to the GraphQL multipart request spec.
import React, { useRef } from 'react'
import { useMutation } from 'graphql-hooks'
const uploadPostPictureMutation = `
mutation UploadPostPicture($picture: Upload!) {
uploadPostPicture(picture: $picture) {
id
pictureUrl
}
}
`
export default function PostForm() {
// File input is always uncontrolled in React.
// See: https://reactjs.org/docs/uncontrolled-components.html#the-file-input-tag.
const fileInputRef = useRef(null)
const [uploadPostPicture] = useMutation(uploadPostPictureMutation)
const handleSubmit = event => {
event.preventDefault()
uploadPostPicture({
variables: {
picture: fileInputRef.current.files[0]
}
})
}
return (
<form onSubmit={handleSubmit}>
<input accept="image/*" ref={fileInputRef} type="file" />
<button>Upload</button>
</form>
)
}
File uploads Node.js
import { FormData } from 'formdata-node'
import { fileFromPath } from 'formdata-node/file-from-path'
const client = new GraphQLClient({
url: 'https://domain.com/graphql',
fetch: require('node-fetch'),
FormData
})
const uploadPostPictureMutation = `
mutation UploadPostPicture($picture: Upload!) {
uploadPostPicture(picture: $picture) {
id
pictureUrl
}
}
`
const { data, error } = await client.request({
query: uploadPostPictureMutation,
variables: { picture: await fileFromPath('some-file.txt') }
})
HTTP Get support
Using GET
for queries can be useful, especially when implementing any sort of HTTP caching strategy. There are two ways you can do this:
Per Query
const { loading, error, data } = useQuery(MY_QUERY, {
fetchOptionsOverrides: { method: 'GET' }
})
// same goes for useManualQuery
const [fetchSomething] = useManualQuery(MY_QUERY, {
fetchOptionsOverrides: { method: 'GET' }
})
For All Queries
When you create your client, set the useGETForQueries
option as true
:
const client = new GraphQLClient({
url: '/graphql',
useGETForQueries: true
})
Authentication
You can have access the to the graphql-hooks client context by using the React's new context API. ClientContext
is actually the result of React.createContext()
.
Login example
import React, { useState, useContext } from 'react'
import { useMutation, ClientContext } from 'graphql-hooks'
const LOGIN_MUTATION = `mutation LoginUser (name: String!, password: String!) {
loginUser(name: $name, password: $password) {
token
}
}`
const Login = () => {
const client = useContext(ClientContext)
const [loginUserMutation] = useMutation(LOGIN_MUTATION)
const [userName, setUserName] = useState()
const [password, setPassword] = useState()
const handleLogin = async e => {
e.preventDefault()
const { data, error } = await loginUserMutation({
variables: { userName, password }
})
if (error) {
// your code to handle login error
} else {
const { token } = data.loginUser
client.setHeader('Authorization', `Bearer ${token}`)
// your code to handle token in browser and login redirection
}
}
return (
<form onSubmit={handleLogin}>
User Name:{' '}
<input
type={'text'}
value={userName}
onChange={e => setUserName(e.target.value)}
/>
PassWord: <input
type={'password'}
value={password}
onChange={e => setPassword(e.target.value)}
/>
<input type={'submit'} value={'Login'} />
</form>
)
}
export default Login
In the above example we use useContext()
hook to get access to the graphql-hooks clientContext.
Then we request the token from the server by performing the loginUser
mutation.
In the case the login is success we set the token to the client's header (client.setHeader
), otherwise we need to handle the error.
For more information about graphql-hooks clientContext refer to GraphQLClient section.
Fragments
Coming soon!
Migrating from Apollo
For a real life example, compare the next.js with-apollo vs with-graphql-hooks. We have feature parity and the main-*.js
bundle is a whopping 93% smaller (7.9KB vs 116KB).
ApolloClient β‘οΈ GraphQLClient
- import { ApolloClient } from 'apollo-client'
- import { InMemoryCache } from 'apollo-cache-inmemory'
+ import { GraphQLClient } from 'graphql-hooks'
+ import memCache from 'graphql-hooks-memcache'
- const client = new ApolloClient({
- uri: '/graphql',
- cache: new InMemoryCache()
- })
+ const client = new GraphQLClient({
+ url: '/graphql',
+ cache: memCache()
+ })
A lot of the options you'd pass to ApolloClient
are the same as GraphQLClient
:
-
uri
β‘οΈurl
-
fetchOptions
-
onError
- the function signature is slightly different -
headers
-
fetch
-
cache
ApolloProvider β‘οΈ ClientContext.Provider
- import { ApolloProvider } from 'react-apollo'
+ import { ClientContext } from 'graphql-hooks'
function App({ client }) {
return (
- <ApolloProvider client={client}>
+ <ClientContext.Provider value={client}>
{/* children */}
+ </ClientContext.Provider>
- </ApolloProvider>
)
}
Query Component β‘οΈ useQuery
- import { Query } from 'react-apollo'
- import gql from 'graphql-tag'
+ import { useQuery } from 'graphql-hooks'
function MyComponent() {
+ const { loading, error, data } = useQuery('...')
- return (
- <Query query={gql`...`}>
- {({ loading, error, data}) => {
if (loading) return 'Loading...'
if (error) return 'Error :('
return <div>{data}</div>
- }}
- </Query>
- )
}
Query Component Props
A lot of options can be carried over as-is, or have direct replacements:
-
query
β‘οΈuseQuery(query)
: Remove any usage ofgql
and pass your queries as strings. -
variables
β‘οΈuseQuery(query, { variables })
-
ssr
β‘οΈuseQuery(query, { ssr })
-
Fetch Policies: See #75 for more info
-
cache-first
: This the default behaviour ofgraphql-hooks
-
cache-and-network
: The refetch function provides this behaviour it will set loading: true, but the old data will be still set until the fetch resolves. -
network-only
β‘οΈuseQuery(QUERY, { skipCache: true })
-
cache-only
: Not supported -
no-cache
β‘οΈuseQuery(QUERY, { useCache: false })
-
Not yet supported
-
errorPolicy
: Any error will set theerror
to be truthy. See useQuery for more details. -
pollInterval
-
notifyOnNetworkStatusChange
-
skip
-
onCompleted
: Similar ability if usinguseManualQuery
-
onError
: Similar ability if usinguseManualQuery
-
partialRefetch
Query Component Render Props
- <Query query={gql`...`}>
- {(props) => {}}
- </Query>
+ const state = useQuery(`...`)
-
props.loading
β‘οΈconst { loading } = useQuery('...')
-
props.error
β‘οΈconst { error } = useQuery('...')
: The error value fromuseQuery
is Boolean the details of the error can be found in either:-
state.fetchError
-
state.httpError
-
state.graphQLErrors
-
-
props.refetch
οΈβ‘οΈconst { refetch } = useQuery('...')
-
props.updateData(prevResult, options)
οΈβ‘οΈstate.updateData(prevResult, newResult)
Not yet supported
-
props.networkStatus
-
props.startPolling
-
props.stopPolling
-
props.subscribeToMore
Mutation Component β‘οΈ useMutation
- import { Mutation } from 'react-apollo'
- import gql from 'graphql-tag'
+ import { useMutation } from 'graphql-hooks'
function MyComponent() {
+ const [mutateFn, { loading, error, data }] = useMutation('...')
- return (
- <Mutation mutation={gql`...`}>
- {(mutateFn, { loading, error }) => {
if (error) return 'Error :('
return <button disabled={loading} onClick={() => mutateFn()}>Submit</button>
- }}
- </Mutation>
- )
}
Mutation Props
-
mutation
β‘οΈuseMutation(mutation)
- no need to wrap it ingql
-
variables
β‘οΈοΈuseMutation(mutation, { variables })
ormutateFn({ variables })
-
ignoreResults
β‘οΈοΈοΈοΈconst [mutateFn] = useMutation(mutation)
-
onCompleted
β‘οΈ οΈmutateFn().then(onCompleted)
-
onError
β‘οΈmutateFn().then(({ error }) => {...})
Not yet supported
-
update
: Coming soon #52 -
optimisticResponse
-
refetchQueries
-
awaitRefetchQueries
-
context
Mutation Component Render Props
- <Mutation mutation={gql`...`}>
- {(mutateFn, props) => {}}
- </Mutation>
+ const [mutateFn, state] = useMutation(`...`)
-
props.data
β‘οΈconst [mutateFn, { data }] = useMutation()
-
props.loading
β‘οΈconst [mutateFn, { loading }] = useMutation()
-
props.error
β‘οΈconst [mutateFn, { error }] = useMutation()
: The the details of the error can be found in either:-
state.fetchError
-
state.httpError
-
state.graphQLErrors
-
-
client
οΈβ‘οΈοΈconst client = useContext(ClientContext)
see ClientContext
Not yet supported
-
called
Testing and mocking
There is a LocalGraphQLClient
class you can use to mock requests without a server for testing or development purposes.
This client inherits from GraphQLClient
and provides the same API, but doesn't connect to any server and instead responds to pre-defined queries.
It needs to be supplied on creation with a localQueries
object, which is an object of query functions.
const localQueries = {
[allPostsQuery]: () => ({
allPosts: [
{
id: 1,
title: 'Test',
url: 'https://example.com'
}
]
}),
[createPostMutation]: () => ({ createPost: { id: 1 } })
}
const client = new LocalGraphQLClient({ localQueries })
const { data, error } = await client.request({
query: 'allPostsQuery'
})
The LocalGraphQLClient
will return data
and error
properties in the same format as the GraphQLClient
Variables
Variables can be used in the local mock queries given to the LocalGraphQLClient
, which can then be supplied to the request
function:
const localQueries = {
AddNumbersQuery: ({ a, b }) => ({
addedNumber: a + b
})
}
const client = new LocalGraphQLClient({ localQueries })
const result = await client.request({
query: 'AddNumbersQuery',
variables: {
a: 2,
b: 3
}
})
console.log(result.data.addedNumber) // Will be 5
Error mocking
Errors can be simply mocked in LocalGraphQLClient
queries by using the LocalGraphQLError
class:
const localQueries = {
ErrorQuery: () =>
new LocalGraphQLError({
httpError: {
status: 404,
statusText: 'Not found',
body: 'Not found'
}
})
}
const client = new LocalGraphQLClient({ localQueries })
const result = await client.request({
query: 'ErrorQuery'
})
console.log(result.error) // The `error` object will have an `httpError`
Testing with React
Example tests that use the LocalGraphQLClient
are provided in the examples/create-react-app/test folder.
The test-utils.js is a good example of how to create a custom render function using @testing-library/react which can wrap the render of a React component in a ClientContext
setup to use the LocalGraphQLClient
with supplied local queries:
const customRender = (ui, options) => {
const client = new LocalGraphQLClient({
localQueries: options.localQueries
})
const Wrapper = ({ children }) => {
return (
<ClientContext.Provider value={client}>{children}</ClientContext.Provider>
)
}
Wrapper.propTypes = {
children: T.node.isRequired
}
return render(ui, {
wrapper: Wrapper,
...options
})
}
export * from '@testing-library/react'
export { customRender as render }
Using this allows to easily render a component using the LocalGraphQLClient
with local queries when writing tests:
// Comes from the above code
import { render, screen } from './test-utils'
const localQueries = {
[allPostsQuery]: () => ({
allPosts: [
{
id: 1,
title: 'Test',
url: 'https://example.com'
}
]
})
}
describe('Posts', () => {
it('should render successfully', async () => {
render(<Posts />, {
localQueries
})
expect(
await screen.findByRole('link', {
name: /Test/i
})
).toBeTruthy()
})
})
Changing mock queries during tests
Because the LocalGraphQLClient
just uses the localQueries
object supplied to it, it is possible to modify or spy the local queries during tests. For example:
it('shows "No posts" if 0 posts are returned', async () => {
jest.spyOn(localQueries, allPostsQuery).mockImplementation(() => ({
allPosts: []
}))
render(<Posts />, {
localQueries
})
expect(await screen.findByText('No posts')).toBeTruthy()
})
Other
Request interceptors
It is possible to provide a custom library to handle network requests. Having that there is more control on how to handle the requests. The following example shows how to supply axios HTTP client with interceptors. It can be handy in the situations where JWT token has expired, needs to be refreshed and request retried.
import axios from 'axios'
import { buildAxiosFetch } from '@lifeomic/axios-fetch'
import { GraphQLClient } from 'graphql-hooks'
const gqlAxios = axios.create()
gqlAxios.interceptors.response.use(
function (response) {
return response
},
function (error) {
// Handle expired JWT and refresh token
}
)
const client = new GraphQLClient({
url: '/graphql',
fetch: buildAxiosFetch(gqlAxios)
})
Community
We now use GitHub Discussions for our community. To join, click on "Discussions". We encourage you to start a new discussion, share some ideas or ask questions from the community. If you want to see the old community posts (on Spectrum) you can access them here.
Contributors
Thanks goes to these wonderful people (emoji key):
Brian Mullan π¬ π π» π π π‘ π€ π§ π β οΈ |
Jack Clark π¬ π π» π π π‘ π€ π§ π β οΈ |
Joe Warren π¬ π π» π π π‘ π€ π§ π β οΈ |
Simone Busoli π¬ π π |
jhey tompkins β οΈ π¬ π π» π π |
Haroen Viaene π |
Ari Bouius π π π» β οΈ |
Klemen KogovΕ‘ek π π€ π» β οΈ |
WΓ©sley Queiroz π π» |
Joseph Thomas π π» β οΈ |
Edvinas Bartkus π» π¬ π π π‘ π€ π§ π β οΈ |
MatΓas Olivera π π» β οΈ π |
tcudok-jg π» |
Martin Adams π |
Gal Dubitski π» π π β οΈ |
Abhishek Shende π» π |
fabienheureux π |
Hugh Boylan π |
Baqer Mamouri π» |
Guillermo Gonzalez π» |
Johan Brook π» π π§ |
Peter Balazs π» π π‘ β οΈ |
Mattia Panzeri π» β οΈ |
Alex Kondratyuk π» β οΈ π π |
Matias Cepeda π |
Jack Huey π π» π β οΈ |
This project follows the all-contributors specification. Contributions of any kind welcome!