
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
graphql-hooks-with-get-support
Advanced tools
🎣 Minimal hooks-first GraphQL client.
npm install graphql-hooks
or
yarn add graphql-hooks
> 1%, not deadConsider polyfilling:
FormDataPromisefetch. NOTE: A custom implementation can also be provided instead of polyfilling, see GraphQLClientFirst 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>
)
}
GraphQLClientUsage:
import { GraphQLClient } from 'graphql-hooks'
const client = new GraphQLClient(config)
config: Object containing configuration properties
url (Required): The url to your GraphQL serverssrMode: Boolean - set to true when using on the server for server-side rendering; defaults to falsecache (Required if ssrMode is true, otherwise optional): Object with the following methods:
cache.get(key)cache.set(key, data)cache.delete(key)cache.clear()cache.keys()getInitialState()fetch(url, options): Fetch implementation - defaults to the global fetch API. Check Request interceptors for more details how to manage fetch.fetchOptions: See MDN for info on what options can be passedheaders: Object, e.g. { 'My-Header': 'hello' }logErrors: Boolean - defaults to trueonError({ operation, result }): Custom error handler
operation: Object with query, variables and operationNameresult: Object containing error, data, fetchError, httpError and graphqlErrorsclient methodsclient.setHeader(key, value): Updates client.headers adding the new header to the existing headersclient.setHeaders(headers): Replaces client.headersclient.removeHeader(key): Updates client.headers removing the header if it existsclient.logErrorResult({ operation, result }): Default error logger; useful if you'd like to use it inside your custom onError handlerrequest(operation, options): Make a request to your GraphQL server; returning a Promise
operation: Object with query, variables and operationNameoptions.fetchOptionsOverrides: Object containing additional fetch options to be added to the default ones passed to new GraphQLClient(config)ClientContextClientContext 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)
}
useQueryUsage:
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 stringoptions: 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.useCache: Boolean - defaults to true; cache the query resultskipCache: Boolean - defaults to false; If true it will by-pass the cache and fetch, but the result will then be cached for subsequent calls. Note the refetch function will do this automaticallyssr: Boolean - defaults to true. Set to false if you wish to skip this query during SSRfetchOptionsOverrides: Object - Specific overrides for this query. See MDN for info on what options can be passedupdateData(previousData, data): Function - Custom handler for merging previous & new query results; return value will replace data in useQuery return value
previousData: Previous GraphQL query or updateData resultdata: New GraphQL query resultuseQuery return valueconst { loading, error, data, refetch, cacheHit, ...errors } = useQuery(QUERY)
loading: Boolean - true if the query is in flighterror: Boolean - true if fetchError or httpError or graphQLErrors has been setdata: Object - the result of your GraphQL queryrefetch(options): Function - useful when refetching the same query after a mutation; NOTE this presets skipCache=true & will bypass the options.updateData function that was passed into useQuery. You can pass a new updateData into refetch if necessary.
options: Object - options that will be merged into the options that were passed into useQuery (see above).cacheHit: Boolean - true if the query result came from the cache, useful for debuggingfetchError: Object - Set if an error occurred during the fetch callhttpError: Object - Set if an error response was returned from the servergraphQLErrors: Array - Populated if any errors occurred whilst resolving the queryuseManualQueryUse 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>
)
}
useMutationMutations unlike Queries are not cached.
Usage:
const [mutationFn, state] = 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 passedSee graphql-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:
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.
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>
)
}
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>
)
}
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 = /* GraphQL */ `
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>
)
}
Coming soon!
Coming soon!
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).
- 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 ➡️ 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 of gql and pass your queries as strings.variables ➡️ useQuery(query, { variables })ssr ➡️ useQuery(query, { ssr })cache-first: This the default behaviour of graphql-hookscache-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 supportedno-cache ➡️ useQuery(QUERY, { useCache: false })Not yet supported
errorPolicy: Any error will set the error to be truthy. See useQuery for more details.pollIntervalnotifyOnNetworkStatusChangeskiponCompleted: Similar ability if using useManualQueryonError: Similar ability if using useManualQuerypartialRefetch- <Query query={gql`...`}>
- {(props) => {}}
- </Query>
+ const state = useQuery(`...`)
props.loading ➡️ const { loading } = useQuery('...')props.error ➡️ const { error } = useQuery('...'): The error value from useQuery is Boolean the details of the error can be found in either:
state.fetchErrorstate.httpErrorstate.graphQLErrorsprops.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 in gqlvariables ➡️️ useMutation(mutation, { variables }) or mutateFn({ 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.graphQLErrorsclient ️➡️️ const client = useContext(ClientContext) see ClientContextNot yet supported
calledIt 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)
})
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
We use BrowserStack to support as many browsers and devices as possible
FAQs
Graphql Hooks
We found that graphql-hooks-with-get-support demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.