- Notifications
You must be signed in to change notification settings - Fork91
🎣 Minimal hooks-first GraphQL client
License
nearform/graphql-hooks
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
🎣 Minimal hooks-first GraphQL client.
- 🥇 First-class hooks API
- ⚖️Tiny bundle: only 22.9kB (7.4kB gzipped)
- 📄 Full SSR support: seegraphql-hooks-ssr
- 🔌 Plugin Caching: seegraphql-hooks-memcache
- 🔥 No more render props hell
- ⏳ Handle loading and error states with ease
npm install graphql-hooks
or
yarn add graphql-hooks
- Node LTS
- Browsers
> 1%, not dead
Consider polyfilling:
FormDataPromisefetch. NOTE: A custom implementation can also be provided instead of polyfilling,seeGraphQLClient
First you'll need to create a client and wrap your app with the provider:
import{GraphQLClient,ClientContext}from'graphql-hooks'constclient=newGraphQLClient({url:'/graphql'})functionApp(){return(<ClientContext.Providervalue={client}>{/* children */}</ClientContext.Provider>)}
Now in your child components you can make use ofuseQuery
import{useQuery}from'graphql-hooks'constHOMEPAGE_QUERY=`query HomePage($limit: Int) { users(limit: $limit) { id name }}`functionMyComponent(){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})=>(<likey={id}>{name}</li>))}</ul>)}
The first thing you may ask when seeinggraphql-hooks is "Why not use Apollo hooks?".It's the comparison most will make. In fact, there's anarticle comparing the two over on LogRocket.
We believegraphql-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, thenapollo-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 |
- API
- Guides
Usage:
import{GraphQLClient}from'graphql-hooks'constclient=newGraphQLClient(config)
config: Object containing configuration properties
url: The URL of your GraphQLHTTP server. If not specified, you must enablefullWsTransportand provide a validsubscriptionClient; otherwise isrequired.fullWsTransport: Boolean - set totrueif you want to usesubscriptionClientto also send query and mutations via WebSocket; defaults tofalsessrMode: Boolean - set totruewhen using on the server for server-side rendering; defaults tofalseuseGETForQueries: Boolean - set totrueto use HTTP GET method for all queries; defaults to false. SeeHTTP Get Support for more infosubscriptionClient: TheWebSocket client configuration. Accepts either an instance ofSubscriptionClientfromsubscriptions-transport-ws orClientfromgraphql-ws. A factory function is also accepted e.g. to avoid the creation of the client in SSR environments.cache(Required ifssrModeistrue, otherwise optional): Object with the following methods:cache.get(key)cache.set(key, data)cache.delete(key)cache.clear()cache.keys()getInitialState()- Seegraphql-hooks-memcache as a reference implementation
fetch(url, options): Fetch implementation - defaults to the globalfetchAPI. CheckRequest interceptors for more details how to managefetch.FormData: FormData implementation - defaults to the globalFormDataAPI. Polyfill this in a node.js environment. Seefile-uploads-nodejs for more info.fetchOptions: SeeMDN for info on what options can be passedheaders: Object, e.g.{ 'My-Header': 'hello' }logErrors: Boolean - defaults totruemiddleware: Accepts an array of middleware functions, default: none, see more inmiddlewares readmeonError({ operation, result }): Custom error handleroperation: Object withquery,variablesandoperationNameresult: Object containingdata,headersanderrorobject that containsfetchError,httpErrorandgraphqlErrors
client.setHeader(key, value): Updatesclient.headersadding the new header to the existing headersclient.setHeaders(headers): Replacesclient.headersclient.removeHeader(key): Updatesclient.headersremoving the header if it existsclient.logErrorResult({ operation, result }): Default error logger; useful if you'd like to use it inside your customonErrorhandlerrequest(operation, options): Make a request to your GraphQL server; returning a Promiseoperation: Object withquery,variablesandoperationName
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 therequestresponse under thedatakey. Example usage:{responseReducer: (data, response) => ({...data, myKey: response.headers.get('content-length)})client.invalidateQuery(query): Will delete the older cache, re-fetch the new data using the same query, and store it in the cache as a new valuequery: The GraphQL query as a plain string to be re-fetched, or an Operation object (withquery,variablesandoperationName)
client.setQueryData(query, (oldState) => [...oldState, newState]]): Will override the older cache state with the new one provided by the function returnquery: The GraphQL query as a plain string, or an Operation object (withquery,variablesandoperationName)(oldState) => [...oldState, newState]]: The callback function with returns will be the new state stored in the cache.oldState: The old value stored in the cache
ClientContext is the result ofReact.createContext() - meaning it can be used directly with React's new context API:
Example:
import{ClientContext}from'graphql-hooks'functionApp(){return(<ClientContext.Providervalue={client}>{/* children can now consume the client context */}</ClientContext.Provider>)}
To access theGraphQLClient instance, callReact.useContext(ClientContext):
importReact,{useContext}from'react'import{ClientContext}from'graphql-hooks'functionMyComponent(){constclient=useContext(ClientContext)}
Usage:
conststate=useQuery(query,[options])
Example:
import{useQuery}from'graphql-hooks'functionMyComponent(){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 unlessquery oroptions.variables changes.
query: Your GraphQL query as a plain string or DocumentNodeoptions: Object with the following optional propertiesvariables: 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; Passtrueif your graphql server supportspersistedflag to serve persisted queries.useCache: Boolean - defaults totrue; cache the query resultskip: Boolean - defaults tofalse; do not execute the query if set totrueskipCache: Boolean - defaults tofalse; Iftrueit will by-pass the cache and fetch, but the result will then be cached for subsequent calls. Note therefetchfunction will do this automaticallyssr: Boolean - defaults totrue. Set tofalseif you wish to skip this query during SSRfetchOptionsOverrides: Object - Specific overrides for this query. SeeMDN for info on what options can be passedupdateData(previousData, data): Function - Custom handler for merging previous & new query results; return value will replacedatainuseQueryreturn valuepreviousData: Previous GraphQL query orupdateDataresultdata: 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 stringrefetchOnMutationError: boolean (optional, defaults totrue) - It indicates whether the query must be re-fetched if the mutation returns an errorfilter: 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
const{ loading, error, data, refetch, cacheHit}=useQuery(QUERY)
loading: Boolean -trueif the query is in flightdata: Object - the result of your GraphQL queryheaders: Object - response headersrefetch(options): Function - useful when refetching the same query after a mutation; NOTE this presetsskipCache=true& will bypass theoptions.updateDatafunction that was passed intouseQuery. You can pass a newupdateDataintorefetchif necessary.options: Object - options that will be merged into theoptionsthat were passed intouseQuery(see above).
cacheHit: Boolean -trueif the query result came from the cache, useful for debuggingerror: Object - Set if at least one of the following errors has occurred and contains:fetchError: Object - Set if an error occurred during thefetchcallhttpError: Object - Set if an error response was returned from the servergraphQLErrors: Array - Populated if any errors occurred whilst resolving the query
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'functionMyComponent(props){const[fetchUser,{ loading, error, data}]=useManualQuery(GET_USER_QUERY,{variables:{id:props.userId}})return(<div><buttononClick={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 theuseManualQuery you can also pass the same options to the query function itself when calling it:
import{useManualQuery}from'graphql-hooks'functionMyComponent(props){const[fetchUser]=useManualQuery(GET_USER_QUERY)constfetchUserThenSomething=async()=>{constuser=awaitfetchUser({variables:{id:props.userId}})returnsomethingElse()}return(<div><buttononClick={fetchUserThenSomething}>Get User!</button></div>)}
Will return the graphql client provided toClientContext.Provider asvalue
Usage:
constclient=useQueryClient()
Example:
import{useQueryClient}from'graphql-hooks'functionMyComponent(){constclient=useQueryClient()return<div>...</div>}
Mutations unlike Queries are not cached.
Usage:
const[mutationFn,state,resetFn]=useMutation(mutation,[options])
Example:
import{useMutation}from'graphql-hooks'constUPDATE_USER_MUTATION=`mutation UpdateUser(id: String!, name: String!) { updateUser(id: $id, name: $name) { name }}`functionMyComponent({ id, name}){const[updateUser]=useMutation(UPDATE_USER_MUTATION)const[newName,setNewName]=useState(name)return(<div><inputtype="text"value={newName}onChange={e=>setNewName(e.target.value)}/><buttononClick={()=>updateUser({variables:{ id,name:newName}})}/></div>)}
Theoptions object that can be passed either touseMutation(mutation, options) ormutationFn(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. SeeMDN for info on what options can be passedclient: GraphQLClient - If a GraphQLClient is explicitly passed as an option, then it will be used instead of the client from theClientContext.onSuccess: A function to be called after the mutation has been finished with success without raising any error
In addition, there is an option to reset the current state before calling the mutation again, by callingresetFn(desiredState) wheredesiredState is optional and if passed, it will override the initial state with:
data: Object - the dataheaders: Object - response headerserror: Error - the errorloading: Boolean - true if it is still loadingcacheHit: Boolean - true if the result was cached
To use subscription you can use eithersubscriptions-transport-ws orgraphql-ws
API
useSubscription(operation, callback)
operation: Object - The GraphQL operation has the following properties:query: String (required) - the GraphQL queryvariables: Object (optional) - Any variables the query might needoperationName: 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.skip: Boolean (optional) - If true, the subscription will not be created.
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 thequick start guide to create the client and provider. Then we need to update the config for ourGraphQLClient passing in thesubscriptionClient:
import{GraphQLClient}from'graphql-hooks'import{SubscriptionClient}from'subscriptions-transport-ws'// orimport{createClient}from'graphql-ws'constclient=newGraphQLClient({url:'http://localhost:8000/graphql',subscriptionClient:()=>newSubscriptionClient('ws://localhost:8000/graphql',{/* additional config options */}),// orsubscriptionClient:()=>createClient({url:'ws://localhost:8000/graphql'/* additional config options */})})
Next, within our React app, we can now make use of theuseSubscription hook.
importReact,{useState}from'react'import{useSubscription}from'graphql-hooks'constTOTAL_COUNT_SUBSCRIPTION=` subscription TotalCount { totalCount { count } }`functionTotalCountComponent(){const[count,setCount]=useState(0)const[error,setError]=useState(null)useSubscription({query:TOTAL_COUNT_SUBSCRIPTION},({ data, errors})=>{if(errors&&errors.length>0){// handle your errorssetError(errors[0])return}// all good, handle the gql resultsetCount(data.totalCount.count)})if(error){return<span>An error occurred{error.message}</span>}return<div>Current count:{count}</div>}
Working Example:
See oursubscription example which has both the client and server code to integrate subscriptions into your application.
See also thefull WS transport example if you want to see how to send every operation through WebSocket.
Seegraphql-hooks-ssr for an in depth guide.
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:
exportconstallPostsQuery=` 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.
Here is an example where we display the paginated queries on separate pages:
import{React,useState}from'react'import{useQuery}from'graphql-hooks'exportdefaultfunctionPostList(){// set a default offset of 0 to load the first pageconst[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}=dataconstareMorePosts=allPosts.length<_allPostsMeta.countreturn(<section><ul>{allPosts.map(post=>(<likey={post.id}><ahref={post.url}>{post.title}</a></li>))}</ul><button// reduce the offset by 10 to fetch the previous pageonClick={()=>setSkipCount(skipCount-10)}disabled={skipCount===0}> Previous page</button><button// increase the offset by 10 to fetch the next pageonClick={()=>setSkipCount(skipCount+10)}disabled={!areMorePosts}> Next page</button></section>)}
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 postsconstupdateData=(prevData,data)=>({ ...data,allPosts:[...prevData.allPosts, ...data.allPosts]})exportdefaultfunctionPostList(){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}=dataconstareMorePosts=allPosts.length<_allPostsMeta.countreturn(<section><ul>{allPosts.map(post=>(<likey={post.id}><ahref={post.url}>{post.title}</a></li>))}</ul>{areMorePosts&&(<button// set the offset to the current number of posts to fetch the next pageonClick={()=>setSkipCount(allPosts.length)}> Show more</button>)}</section>)}
We can have a query to automatically refetch when any mutation from a provided list is executed.In the following example we are refetching a list of posts for a given user.
Example
exportconstallPostsByUserIdQuery=` query allPosts($userId: Int!) { allPosts(userId: $userId) { id title url } }`exportconstcreatePostMutation=` mutation createPost($userId: Int!, $text: String!) { createPost(userId: $userId, text: $text) { id title url } }`constmyUserId=5useQuery(allPostsByUserIdQuery,{variables:{userId:myUserId},refetchAfterMutations:[{mutation:createPostMutation,filter:variables=>variables.userId===myUserId}]})
There are two ways to reach that:
import{useMutation,useQueryClient}from'graphql-hooks'importReactfrom'react'constMY_MUTATION=`...`constMY_QUERY=`...`exportdefaultfunctionMyComponent(){constclient=useQueryClient()const[applyMutation,{ ...}]=useMutation(MY_MUTATION,{onSuccess:()=>client.invalidateQuery(MY_QUERY)})return( ...)}
import{useMutation,useQueryClient}from'graphql-hooks'importReactfrom'react'constMY_MUTATION=`...`constMY_QUERY=`...`exportdefaultfunctionMyComponent(){constclient=useQueryClient()const[applyMutation,{ ...}]=useMutation(MY_MUTATION,{onSuccess:(result)=>{client.setQueryData(MY_QUERY,oldState=>[ ...oldState,result,])}})return( ...)}
graphql-hooks complies with theGraphQL multipart request spec, allowing files to be used as query or mutation arguments. The same spec is also supported by popular GraphQL servers, includingApollo Server (see list of supported servershere).
If there are files to upload, the request's body will be aFormData instance conforming to the GraphQL multipart request spec.
importReact,{useRef}from'react'import{useMutation}from'graphql-hooks'constuploadPostPictureMutation=` mutation UploadPostPicture($picture: Upload!) { uploadPostPicture(picture: $picture) { id pictureUrl } }`exportdefaultfunctionPostForm(){// File input is always uncontrolled in React.// See: https://reactjs.org/docs/uncontrolled-components.html#the-file-input-tag.constfileInputRef=useRef(null)const[uploadPostPicture]=useMutation(uploadPostPictureMutation)consthandleSubmit=event=>{event.preventDefault()uploadPostPicture({variables:{picture:fileInputRef.current.files[0]}})}return(<formonSubmit={handleSubmit}><inputaccept="image/*"ref={fileInputRef}type="file"/><button>Upload</button></form>)}
import{FormData}from'formdata-node'import{fileFromPath}from'formdata-node/file-from-path'constclient=newGraphQLClient({url:'https://domain.com/graphql',fetch:require('node-fetch'), FormData})constuploadPostPictureMutation=` mutation UploadPostPicture($picture: Upload!) { uploadPostPicture(picture: $picture) { id pictureUrl } }`const{ data, error}=awaitclient.request({query:uploadPostPictureMutation,variables:{picture:awaitfileFromPath('some-file.txt')}})
UsingGET 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 useManualQueryconst[fetchSomething]=useManualQuery(MY_QUERY,{fetchOptionsOverrides:{method:'GET'}})
For All Queries
When you create your client, set theuseGETForQueries option astrue:
constclient=newGraphQLClient({url:'/graphql',useGETForQueries:true})
You can have access to the graphql-hooks client context by using React's new context API.ClientContext is actually the result ofReact.createContext().
Login Example
importReact,{useState,useContext}from'react'import{useMutation,ClientContext}from'graphql-hooks'constLOGIN_MUTATION=`mutation LoginUser (name: String!, password: String!) { loginUser(name: $name, password: $password) { token }}`constLogin=()=>{constclient=useContext(ClientContext)const[loginUserMutation]=useMutation(LOGIN_MUTATION)const[userName,setUserName]=useState()const[password,setPassword]=useState()consthandleLogin=asynce=>{e.preventDefault()const{ data, error}=awaitloginUserMutation({variables:{ userName, password}})if(error){// your code to handle login error}else{const{ token}=data.loginUserclient.setHeader('Authorization',`Bearer${token}`)// your code to handle token in browser and login redirection}}return(<formonSubmit={handleLogin}> User Name:{' '}<inputtype={'text'}value={userName}onChange={e=>setUserName(e.target.value)}/> PassWord:<inputtype={'password'}value={password}onChange={e=>setPassword(e.target.value)}/><inputtype={'submit'}value={'Login'}/></form>)}exportdefaultLogin
In the above example we useuseContext() hook to get access to the graphql-hooks clientContext.Then we request the token from the server by performing theloginUser mutation.In the case the login is successful 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 toGraphQLClient section.
Coming soon!
For a real life example, compare the next.jswith-apollo vswith-graphql-hooks. We have feature parity and themain-*.js bundle is a whopping93% smaller (7.9KB vs 116KB).
- 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 toApolloClient are the same asGraphQLClient:
uri➡️urlfetchOptionsonError- the function signature is slightly differentheadersfetchcache
- 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> )}
- 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>- )}
A lot of options can be carried over as-is, or have direct replacements:
query➡️useQuery(query): Remove any usage ofgqland pass your queries as strings.variables➡️useQuery(query, { variables })ssr➡️useQuery(query, { ssr })- Fetch Policies: See#75 for more info
cache-first: This is the default behaviour ofgraphql-hookscache-and-network: The refetch function provides this behavior 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 supportedno-cache➡️useQuery(QUERY, { useCache: false })
Not yet supported
errorPolicy: Any error will set theerrorto be truthy. SeeuseQuery for more details.pollIntervalnotifyOnNetworkStatusChangeskiponCompleted: Similar ability if usinguseManualQueryonError: Similar ability if usinguseManualQuerypartialRefetch
- <Query query={gql`...`}>- {(props) => {}}- </Query>+ const state = useQuery(`...`)
props.loading➡️const { loading } = useQuery('...')props.error➡️const { error } = useQuery('...'): The error value fromuseQueryis Boolean the details of the error can be found in either:state.fetchErrorstate.httpErrorstate.graphQLErrors
props.refetch️➡️const { refetch } = useQuery('...')props.updateData(prevResult, options)️➡️state.updateData(prevResult, newResult)
Not yet supported
props.networkStatusprops.startPollingprops.stopPollingprops.subscribeToMore
- 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➡️useMutation(mutation)- no need to wrap it ingqlvariables➡️️useMutation(mutation, { variables })ormutateFn({ variables })ignoreResults➡️️️️const [mutateFn] = useMutation(mutation)onCompleted➡️ ️mutateFn().then(onCompleted)onError➡️mutateFn().then(({ error }) => {...})
Not yet supported
update: Coming soon#52optimisticResponserefetchQueriesawaitRefetchQueriescontext
- <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.fetchErrorstate.httpErrorstate.graphQLErrors
client️➡️️const client = useContext(ClientContext)seeClientContext
Not yet supported
called
There is aLocalGraphQLClient class you can use to mock requests without a server for testing or development purposes.
This client inherits fromGraphQLClient 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 alocalQueries object, which is an object where:
- the keys are the queries defined in the application;
- the values are query functions returning the mocked data.
// src/components/Post.jsexportconstallPostsQuery=` query { allPosts { id title url } }`
// test/Post.test.tsximport{allPostsQuery,createPostMutation}from'../src/components/Post'constlocalQueries={[allPostsQuery]:()=>({allPosts:[{id:1,title:'Test',url:'https://example.com'}]}),[createPostMutation]:()=>({createPost:{id:1}})}constclient=newLocalGraphQLClient({ localQueries})const{ data, error}=awaitclient.request({query:allPostsQuery})
TheLocalGraphQLClient will returndata anderror properties in the same format as theGraphQLClient
Variables can be used in the local mock queries given to theLocalGraphQLClient, which can then be supplied to therequest function:
constlocalQueries={AddNumbersQuery:({ a, b})=>({addedNumber:a+b})}constclient=newLocalGraphQLClient({ localQueries})constresult=awaitclient.request({query:'AddNumbersQuery',variables:{a:2,b:3}})console.log(result.data.addedNumber)// Will be 5
Errors can be simply mocked inLocalGraphQLClient queries by using theLocalGraphQLError class:
// test/Post.test.tsximport{allPostsQuery}from'../src/components/Post'constlocalQueries={[allPostsQuery]:()=>newLocalGraphQLError({httpError:{status:404,statusText:'Not found',body:'Not found'}})}constclient=newLocalGraphQLClient({ localQueries})constresult=awaitclient.request({query:allPostsQuery})console.log(result.error)// The `error` object will have an `httpError`
It is also possible to mock a partial error response (for example where one resolver encounters an error but other resolvers return successfully). To do this, includeError objects in the mock query resolver:
import{allPostsQuery}from'../src/components/Post'constlocalQueries={[allPostsQuery]:()=>({field1:'foo',field2:newError('something went wrong'),nested:{field3:newError('a nested error')}})}constclient=newLocalGraphQLClient({ localQueries})constresult=awaitclient.request({query:allPostsQuery})console.log(result.data)// The `data` object will have the correct value for `field1` and `null` for any fields returning `Error` objectsconsole.log(result.error)// The `error` object will have a `graphQLErrors` array containing each of the `Error` objects created above
Example tests that use theLocalGraphQLClient are provided inthe examples/create-react-app/test folder.
Thetest-utils.tsx 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 aClientContext setup to use theLocalGraphQLClient with supplied local queries:
constcustomRender=(ui,options)=>{constclient=newLocalGraphQLClient({localQueries:options.localQueries})constWrapper=({ children})=>{return(<ClientContext.Providervalue={client}>{children}</ClientContext.Provider>)}Wrapper.propTypes={children:T.node.isRequired}returnrender(ui,{wrapper:Wrapper, ...options})}export*from'@testing-library/react'export{customRenderasrender}
Using this allows us to easily render a component using theLocalGraphQLClient with local queries when writing tests:
// Comes from the above codeimport{render,screen}from'./test-utils'constlocalQueries={[allPostsQuery]:()=>({allPosts:[{id:1,title:'Test',url:'https://example.com'}]})}describe('Posts',()=>{it('should render successfully',async()=>{render(<Posts/>,{ localQueries})expect(awaitscreen.findByRole('link',{name:/Test/i})).toBeTruthy()})})
Because theLocalGraphQLClient just uses thelocalQueries 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(awaitscreen.findByText('No posts')).toBeTruthy()})
All client methods support the ability to provide type information for response data, query variables and error responses.
import{useQuery}from'graphql-hooks'typeUser={id:stringname:string}typeCustomError={message:stringextensions?:Record<string,any>}constHOMEPAGE_QUERY=`query HomePage($limit: Int) { users(limit: $limit) { id name }}`functionMyComponent(){const{ loading, error, data}=useQuery<User,{limit:number},CustomError>(HOMEPAGE_QUERY,{variables:{limit:10}})if(loading)return'Loading...'if(error)return'Something Bad Happened'return(<ul>{data.users.map(({ id, name})=>(<likey={id}>{name}</li>))}</ul>)}
graphql-hooks also supportsTypedDocumentNode. This allows you to use GraphQL code gen to createDocumentNodes for your GQL queries and receive full type support.
import{useQuery}from'graphql-hooks'import{graphql}from'./gql'constHOMEPAGE_QUERY=graphql(` query HomePage($limit: Int) { users(limit: $limit) { id name } }`)functionMyComponent(){// data will be typed as User objects with id, name propertiesconst{ 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})=>(<likey={id}>{name}</li>))}</ul>)}
Full details of the features ofTypedDocumentNode and GraphQL Code Generator can be foundhere. Full examples of this implementation are in the examples folder.
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.
importaxiosfrom'axios'import{buildAxiosFetch}from'@lifeomic/axios-fetch'import{GraphQLClient}from'graphql-hooks'constgqlAxios=axios.create()gqlAxios.interceptors.response.use(function(response){returnresponse},function(error){// Handle expired JWT and refresh token})constclient=newGraphQLClient({url:'/graphql',fetch:buildAxiosFetch(gqlAxios)})
if you wish to abort a fetch it is possible to pass an AbortController signal to thefetchOptionsOverrides option of the fetch function. This is notgraphql-hooks specific functionality, rather just an example of how to use it with the library.
import{useManualQuery}from'graphql-hooks'functionAbortControllerExample(){constabortControllerRef=useRef()const[fetchData,{ loading}]=useManualQuery(`...`)consthandleFetch=()=>{abortControllerRef.current=newAbortController()const{ signal}=abortControllerRef.currentfetchData({fetchOptionsOverrides:{ signal}})}consthandleAbort=()=>{abortControllerRef.current?.abort()}return(<><buttononClick={handleFetch}>Fetch Data</button>{loading&&<buttononClick={handleAbort}>Abort</button>}</>)}
As well as supporting input of your queries as strings, this library also supports using aDocumentNode. Document nodes can be generated using a code-generation tool such asGraphQL codegen which will provide typing information for your queries based on your GraphQL schema (see the typescript example). If you don't want to use a code-generation library you can usegraphql-tag to generate aDocumentNode.
importgqlfrom'graphql-tag'constallPostsQuery=gql` query { posts { id name } }`functionPosts(){const{ loading, error, data, refetch}=useQuery(allPostsQuery)return(<><h2>Add post</h2><AddPost/><h2>Posts</h2><buttononClick={()=>refetch()}>Reload</button><PostListloading={loading}error={error}data={data}/></>)}...
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 themhere.
Thanks goes to these wonderful people (emoji key):
This project follows theall-contributors specification. Contributions of any kind are welcome!
About
🎣 Minimal hooks-first GraphQL client
Topics
Resources
License
Code of conduct
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.