GithubHelp home page GithubHelp logo

nearform / graphql-hooks Goto Github PK

View Code? Open in Web Editor NEW
1.9K 114.0 89.0 8.25 MB

🎣 Minimal hooks-first GraphQL client

License: Other

JavaScript 10.37% TypeScript 89.63%
hooks react-hooks react graphql graphql-client hacktoberfest

graphql-hooks's Introduction

graphql-hooks

ci Coverage Status bundlephobia npm lerna

🎣 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

Consider polyfilling:

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

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 enable fullWsTransport and provide a valid subscriptionClient; otherwise is required.
  • fullWsTransport: Boolean - set to true if you want to use subscriptionClient to also send query and mutations via WebSocket; defaults to false
  • ssrMode: Boolean - set to true when using on the server for server-side rendering; defaults to false
  • useGETForQueries: Boolean - set to true 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 of SubscriptionClient from subscriptions-transport-ws or Client from graphql-ws. A factory function is also accepted e.g. to avoid the creation of the client in SSR environments.
  • cache (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()
    • See graphql-hooks-memcache as a reference implementation
  • fetch(url, options): Fetch implementation - defaults to the global fetch API. Check Request interceptors for more details how to manage fetch.
  • FormData: FormData implementation - defaults to the global FormData 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 to true
  • middleware: Accepts an array of middleware functions, default: none, see more in middlewares readme
  • onError({ operation, result }): Custom error handler
    • operation: Object with query, variables and operationName
    • result: Object containing data and error object that contains fetchError, httpError and graphqlErrors

client methods

  • client.setHeader(key, value): Updates client.headers adding the new header to the existing headers
  • client.setHeaders(headers): Replaces client.headers
  • client.removeHeader(key): Updates client.headers removing the header if it exists
  • client.logErrorResult({ operation, result }): Default error logger; useful if you'd like to use it inside your custom onError handler
  • request(operation, options): Make a request to your GraphQL server; returning a Promise
    • operation: Object with query, variables and operationName
  • options.fetchOptionsOverrides: Object containing additional fetch options to be added to the default ones passed to new GraphQLClient(config)
  • options.responseReducer: Reducer function to pick values from the original Fetch Response object. Values are merged to the request response under the data key. 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 value
    • query: The GraphQL query as a plain string to be re-fetched, or an Operation object (with query, variables and operationName)
  • client.setQueryData(query, (oldState) => [...oldState, newState]]): Will override the older cache state with the new one provided by the function return
    • query: The GraphQL query as a plain string, or an Operation object (with query, variables and operationName)
    • (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

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 or DocumentNode
  • 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 to false; Pass true if your graphql server supports persisted flag to serve persisted queries.
    • useCache: Boolean - defaults to true; cache the query result
    • skip: Boolean - defaults to false; do not execute the query if set to true
    • skipCache: 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 automatically
    • ssr: Boolean - defaults to true. Set to false 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 replace data in useQuery return value
      • previousData: Previous GraphQL query or updateData 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 the ClientContext.
    • 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
        • refetchOnMutationError: boolean (optional, defaults to true) - It indicates whether the query must be re-fetched if the mutation returns an error
        • 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 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 debugging
  • error: Object - Set if at least one of the following errors has occurred and contains:
    • fetchError: Object - Set if an error occurred during the fetch 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>
  )
}

useQueryClient

Will return the graphql client provided to ClientContext.Provider as value

Usage:

const client = useQueryClient()

Example:

import { useQueryClient } from 'graphql-hooks'

function MyComponent() {
  const client = useQueryClient()

  return <div>...</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 the ClientContext.
  • 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 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 has 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 the ClientContext.
  • 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 provider. 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 is executed. 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
    }
  ]
})

Manually updating the cache after some mutation

There are two ways to reach that:

By re-fetching the query

import { useMutation, useQueryClient } from 'graphql-hooks'
import React from 'react'

const MY_MUTATION = `...`
const MY_QUERY = `...`

export default function MyComponent() {
  const client = useQueryClient()
  const [applyMutation, { ... }] = useMutation(MY_MUTATION, {
    onSuccess: () => client.invalidateQuery(MY_QUERY)
  })

  return (
    ...
  )
}

By overriding the old state in the cache without re-fetching data

import { useMutation, useQueryClient } from 'graphql-hooks'
import React from 'react'

const MY_MUTATION = `...`
const MY_QUERY = `...`

export default function MyComponent() {
  const client = useQueryClient()
  const [applyMutation, { ... }] = useMutation(MY_MUTATION, {
    onSuccess: (result) => {
      client.setQueryData(MY_QUERY, oldState => [
        ...oldState,
        result,
      ])
    }
  })

  return (
    ...
  )
}

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 to the graphql-hooks client context by using 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 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 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 of gql and 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 of graphql-hooks
    • cache-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 supported
    • no-cache ➡️ useQuery(QUERY, { useCache: false })

Not yet supported

  • errorPolicy: Any error will set the error to be truthy. See useQuery for more details.
  • pollInterval
  • notifyOnNetworkStatusChange
  • skip
  • onCompleted: Similar ability if using useManualQuery
  • onError: Similar ability if using useManualQuery
  • 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 from useQuery 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 in gql
  • variables ➡️️ 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 #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 where:

  • the keys are the queries defined in the application;
  • the values are query functions returning the mocked data.
// src/components/Post.js
export const allPostsQuery = `
  query {
    allPosts {
      id
      title
      url
    }
  }
`
// test/Post.test.tsx
import { allPostsQuery, createPostMutation } from '../src/components/Post'

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:

// test/Post.test.tsx
import { allPostsQuery } from '../src/components/Post'

const localQueries = {
  [allPostsQuery]: () =>
    new LocalGraphQLError({
      httpError: {
        status: 404,
        statusText: 'Not found',
        body: 'Not found'
      }
    })
}
const client = new LocalGraphQLClient({ localQueries })
const result = await client.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, include Error objects in the mock query resolver:

import { allPostsQuery } from '../src/components/Post'

const localQueries = {
  [allPostsQuery]: () => ({
    field1: 'foo',
    field2: new Error('something went wrong'),
    nested: {
      field3: new Error('a nested error')
    }
  })
}
const client = new LocalGraphQLClient({ localQueries })
const result = await client.request({
  query: allPostsQuery
})
console.log(result.data) // The `data` object will have the correct value for `field1` and `null` for any fields returning `Error` objects
console.log(result.error) // The `error` object will have a `graphQLErrors` array containing each of the `Error` objects created above

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 us 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()
})

Typescript Support

All client methods support the ability to provide type information for response data, query variables and error responses.

import { useQuery } from 'graphql-hooks'

type User = {
  id: string
  name: string
}

type CustomError = {
  message: string
  extensions?: Record<string, any>
}

const HOMEPAGE_QUERY = `query HomePage($limit: Int) {
  users(limit: $limit) {
    id
    name
  }
}`

function MyComponent() {
  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 }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  )
}

graphql-hooks also supports TypedDocumentNode. This allows you to use GraphQL code gen to create DocumentNodes for your GQL queries and receive full type support.

import { useQuery } from 'graphql-hooks'
import { graphql } from './gql'

const HOMEPAGE_QUERY = graphql(`query HomePage($limit: Int) {
  users(limit: $limit) {
    id
    name
  }
}`)

function MyComponent() {
  // data will be typed as User objects with id, name properties
  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>
  )
}

Full details of the features of TypedDocumentNode and GraphQL Code Generator can be found here. Full examples of this implementation are in the examples folder.

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)
})

AbortController

if you wish to abort a fetch it is possible to pass an AbortController signal to the fetchOptionsOverrides option of the fetch function. This is not graphql-hooks specific functionality, rather just an example of how to use it with the library.

import { useManualQuery } from 'graphql-hooks'

function AbortControllerExample() {
  const abortControllerRef = useRef()
  const [fetchData, { loading }] = useManualQuery(`...`)

  const handleFetch = () => {
    abortControllerRef.current = new AbortController()
    const { signal } = abortControllerRef.current
    fetchData({
      fetchOptionsOverrides: {
        signal
      }
    })
  }

  const handleAbort = () => {
    abortControllerRef.current?.abort()
  }

  return (
    <>
      <button onClick={handleFetch}>Fetch Data</button>
      {loading && <button onClick={handleAbort}>Abort</button>}
    </>
  )
}

GraphQL Document Support

As well as supporting input of your queries as strings, this library also supports using a DocumentNode. Document nodes can be generated using a code-generation tool such as GraphQL 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 use graphql-tag to generate a DocumentNode.

import gql from 'graphql-tag'

const allPostsQuery = gql`
  query {
     posts {
      id
      name
     }
  }
`

function Posts() {
  const { loading, error, data, refetch } = useQuery(allPostsQuery)

  return (
    <>
      <h2>Add post</h2>
      <AddPost />
      <h2>Posts</h2>
      <button onClick={() => refetch()}>Reload</button>
      <PostList loading={loading} error={error} data={data} />
    </>
  )
}

...

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
Brian Mullan

💬 🐛 💻 🖋 📖 💡 🤔 🚧 👀 ⚠️
Jack Clark
Jack Clark

💬 🐛 💻 🖋 📖 💡 🤔 🚧 👀 ⚠️
Joe Warren
Joe Warren

💬 🐛 💻 🖋 📖 💡 🤔 🚧 👀 ⚠️
Simone Busoli
Simone Busoli

💬 🐛 📖
jhey tompkins
jhey tompkins

⚠️ 💬 🐛 💻 🖋 👀
Haroen Viaene
Haroen Viaene

🐛
Ari Bouius
Ari Bouius

📖 🐛 💻 ⚠️
Klemen Kogovšek
Klemen Kogovšek

🐛 🤔 💻 ⚠️
Wésley Queiroz
Wésley Queiroz

🐛 💻
Joseph Thomas
Joseph Thomas

🐛 💻 ⚠️
Edvinas Bartkus
Edvinas Bartkus

💻 💬 🐛 📖 💡 🤔 🚧 👀 ⚠️
Matías Olivera
Matías Olivera

🐛 💻 ⚠️ 📖
tcudok-jg
tcudok-jg

💻
Martin Adams
Martin Adams

📖
Gal Dubitski
Gal Dubitski

💻 🐛 📖 ⚠️
Abhishek Shende
Abhishek Shende

💻 🐛
fabienheureux
fabienheureux

👀
Hugh Boylan
Hugh Boylan

👀
Baqer Mamouri
Baqer Mamouri

💻
Guillermo Gonzalez
Guillermo Gonzalez

💻
Johan Brook
Johan Brook

💻 🐛 🚧
Peter Balazs
Peter Balazs

💻 📖 💡 ⚠️
Mattia Panzeri
Mattia Panzeri

💻 ⚠️
Alex Kondratyuk
Alex Kondratyuk

💻 ⚠️ 📖 🐛
Matias Cepeda
Matias Cepeda

📖
Jack Huey
Jack Huey

🐛 💻 📖 ⚠️
Roman Zanettin
Roman Zanettin

💻 ⚠️
Francesco Maida
Francesco Maida

🚧

This project follows the all-contributors specification. Contributions of any kind are welcome!

graphql-hooks's People

Contributors

allcontributors[bot] avatar andreaforni avatar bmullan91 avatar brainrepo avatar bredikhin avatar brookback avatar csorfab avatar dependabot[bot] avatar edvinasbartkus avatar ericbf avatar giacomorebonato avatar ianlnf avatar ilteoood avatar jackdclark avatar jh3y avatar joezo avatar luke88jones avatar melkornemesis avatar olistic avatar ovhemert avatar radomird avatar renovate[bot] avatar rp4rk avatar salmanm avatar simoneb avatar stearm avatar stefanoruth avatar vaclav-zeman avatar williamlines avatar zanettin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-hooks's Issues

Running tests yields errors

Environment

  • graphql-hooks version: 3.1.1
  • react version: 16.8.3
  • Browser: N/A

Description

Running npm run test yields several errors although the tests pass.

Warning: An update to TestHook inside a test was not wrapped in act(...).

      When testing, code that causes React state updates should be wrapped into act(...):

      act(() => {
        /* fire events that update state */
      });
      /* assert on the output */

      This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act

I believe the errors are printed when running useQuery.test.js and useClientRequest.test.js.

How to reproduce

npm run test

Suggested solution (optional)

  • Potential versioning issue?

Discuss, Slack...Discord channel :D

Maybe some tool to spread love, this library is great and I think will have future, please make some chat around :)

Also, somebody has a NextJs boilerplate to share with graphql-hooks :v ?
Thanks guys, you're doing a great thing here.

SSR Support

  • graphql-hooks-ssr module
  • Should support nested queries
  • Should handle ssr errors
  • Should pre-populate cache to be used for rehydration on the client

Add test coverage for graphql-hooks-ssr

Package

  • graphql-hooks
  • graphql-hooks-ssr
  • graphql-hooks-memcache

Description

There is currently no test coverage for graphql-hooks-ssr, we should add some.

Test fetchData from useClientRequest

See

it.skip('needs testing', () => {
// fetchData updates the state & therefore needs to be wrapped inside an `act`
// https://reactjs.org/docs/test-utils.html#act.
//
// Warning: An update to TestHook inside a test was not wrapped in act(...).
// When testing, code that causes React state updates should be wrapped into act(...):
// act(() => {
// /* fire events that update state */
// });
// / * assert on the output */
// This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
// in TestHook
// in Wrapper
//
// fetchData is an async function, which `act` does not currently support
// see https://github.com/facebook/react/issues/14769
//
// support is currently being implemented
// see https://github.com/facebook/react/pull/14853

Add ability to skip query entirely

Description

In some cases one could skip making the request to graphql if some condition is true. For example if you have two cascade dropdowns on initial render you want to load and display data only in the first one and the second one just display it as disabled. When you pick something in the first dropdown you have to query the data for the second dropdown.

In this example the second dropdown will skip making the request to get the data until the condition to have selected some value in the first dropdown is met.

Suggested implementation

A skip boolean prop could be set as part of useQuery opts and if true the entire query will be skipped and it will return data as undefined

Local set up included in contributing guidelines/getting started

Description

For a fresh contributor there are no guidelines on how to set up the project in order to develop against it. This may be related to #40.

On a fresh clone, being able to jump straight into development would be ideal 👍

Suggested implementation

Extra information included in the CONTRIBUTING.md file either pointing at starter apps or with instructions on how to set up as a monorepo?

useClientRequest returns state for previous request

This cropped up when testing out useQuery with different variables after the initial request. It looks like useClientRequest returns data the second time it's called, regardless of the query/options that were passed in.

Add unit tests

Add unit test coverage for all files, there are currently none

---------------------|----------|----------|----------|----------|-------------------|
File                 |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------------|----------|----------|----------|----------|-------------------|
All files            |    17.43 |        0 |        0 |    17.59 |                   |
 ClientContext.js    |      100 |      100 |      100 |      100 |                   |
 GraphQLClient.js    |     1.85 |        0 |        0 |     1.89 |... 29,134,135,138 |
 index.js            |      100 |      100 |      100 |      100 |                   |
 useClientRequest.js |    14.29 |        0 |        0 |    14.29 |... 0,91,94,99,102 |
 useQuery.js         |    27.78 |        0 |        0 |    27.78 |... 27,28,29,33,35 |
---------------------|----------|----------|----------|----------|-------------------|```

README Updates

Package

  • graphql-hooks

Environment

  • graphql-hooks version: v3.2.3 after lerna

Description

Publishing README.md only updates are tricky... The ~/packages/graphql-hooks/README.md is ignored the changes are copied over during prepublishOnly.

Here's how I got it working:

  1. Force a new version of graphql-hooks
    lerna version --conventional-commits --force-publish=graphql-hooks
  2. Publish to npm
    NPM_CONFIG_OTP=<code> lerna publish from-package

Proposed Changes

  1. Leave things as-is but document steps in CONTRIBUTING.md
  2. Change ~/packages/graphql-hooks/README.md to only be a link to the top-level README
  3. Move the top-level README into ~/packages/graphql-hooks, change top-level one to be more overview / informational with links to sub-packages. Example https://github.com/ReactTraining/react-router

Docs: Authentication Guide

  • Typical use case of setting a JWT via client.setHeader()
  • Include a login component that will call a login function which returns the token (could be graphql query)
  • Demonstration accessing the client via ClientContext
  • Calling setHeader with JWT
  • Refetching queries after login

Update Renovate Config to Update Examples

Package

  • examples/*

Description

React doesn't like it if there are two versions within a bundle and errors. This happened recently when we updated the packages react version in #170 - but failed to also update the examples/* package.json.

How to reproduce

n/a

Suggested solution (optional)

Update the renovate.json to override the default behaviour that ignores the examples directory.

I think it's coming from https://renovatebot.com/docs/presets-default/ which includes the following:

{
  "ignorePaths": [
    "**/node_modules/**",
    "**/bower_components/**",
    "**/vendor/**",
    "**/examples/**",
    "**/__tests__/**",
    "**/test/**",
    "**/tests/**"
  ]
}

Add ability to require GraphQLClient without having React in scope

Description

Even though the library is about React hooks, it provides a generic GraphQL client that doesn't really need React to work.

Because of the way the modules are bundled, it's currently not possible to require the GraphQLClient module alone, meaning that you have to import the whole module bundle, hence creating a dependency on React.

Suggested implementation

Change the rollup configuration so that it outputs not just a single output module, but one for the GraphQLClient as well, so that it can be required as a standalone module.

useManualQuery returns state.loading=true by default

Package

graphql-hooks

Environment

  • graphql-hooks version: 3.3.1

Description

Calling useManualQuery currently returns a state with loading set to true, even though the request function has not been called.

How to reproduce

  • Call useManualQuery
  • Inspect state

Docs: Migrating from Apollo Guide

  • ApolloClient ➡️GraphQLClient
  • <Query /> ➡️ useQuery
  • <Mutation /> ➡️useMutation
  • client.query ➡️useManualQuery
  • SSR getDataFromTree ➡️getIntialState

Docs: Keep data from previous fetch

Description

Let's say useQuery is called based on the live contents of an input field (e.g. a filter/search box). By default, data gets set to undefined when the query arguments change, causing rendered content to flash in and out of existence – this doesn't look good. In order to keep the state from the previous fetch, updateData needs to be specified in the useQuery options (can be as trivial as { updateData: (data, nextData) => nextData }), but that's a bit unintuitive because it doesn't really explain what is actually happening under the hood (RESET_STATE isn't fired), and I'm not actually doing anything useful with updateData, like pagination.

if (!initialOpts.updateData) {
// if using updateData we can assume that the consumer cares about the previous data
dispatch({ type: actionTypes.RESET_STATE, initialState })

I'm not sure if this is a common enough use case to warrant any changes, but it might be worth adding to the docs that this is what setting updateData does.

Make `cache` option required for SSR

Package

graphql-hooks-ssr

Environment

  • graphql-hooks-ssr version: 1.0.5

Description

When following the example found here, it is not immediately obvious that using a caching plugin is required. Some users might choose to begin with the simplest approach possible, and omit a cache provider.

How to reproduce

  • Follow the "Quick Start" example for graphql-hooks-ssr
  • Don't provide a cache options to GraphQLClient

Suggested solution (optional)

  • Update documentation to reflect that the cache option is required, or alternatively, update the code to not actually require it.

Requests are not cancelled on unmount

Environment

  • graphql-hooks version: 3.1.0
  • react version: 16.8.3
  • Browser: Any

Description

Requests are not cancelled on unmount which means we try to update state after component is gone.

screenshot 2019-02-25 at 14 51 44

How to reproduce

Can be reproduced in the example next app by simulating slow connections and upvoting a post, then navigating to the about page before the request returns.

Suggested solution (optional)

Keep track of whether the component is mounted by making useClientRequest return a method to cancel everything. This would switch an internal var that we would check once the request has returned, so that we don't then try to set state.

Add useDebugValue

Description

I would like this package to add useDebugValue to hooks and make them simple to track in dev tools. I think just showing Loading/Loaded/Error would be enough for useQuery to give a user a sense of what's going on with a hook. Current implementation gives out a few nested hooks without showing relevant data (that's how dev tools are with complex data), so just showing a status would be an improvement.

Suggested implementation

I can implement that upon discussion and send a PR.

Setup testing

  • basic react-testing-library + jest
  • CircleCI + badge
  • Stub out other tests with empty it('should...')
  • Coverage + badge

Refetch queries with mutations subscription

Hello,

Description

One important feature of a graphql-client to me is the ability to refetch/update the data of a query after one or multiple mutations executions.

I really liked the idea of "a query subscribes to one or more mutations" here (see onMutation parameter) : https://github.com/arackaf/micro-graphql-react#building-queries

I started playing with the idea that we could leverage the useEffect wrapping the query fetching call and pass extra inputs to its second argument to automatically refetch the query again.

The extra inputs would take the form of a list of mutations results so whenever a subcribed mutation happens, the extra inputs changed and the query is fired again.

Suggested implementation

useQuery(GET_TODO, { id }, { refetch: ['addTodo', 'updateTodo', 'deleteTodo'] })

I made a working example of the idea here : https://codesandbox.io/s/m3nq0315m9

The general idea is to maintain of global registry of mutations results in the Context.
From here, useQuery could accept a refetch options taking a list of mutations names. Then it's just a matter of mapping the list of mutations names into a list of mutations results, and voilà, we have our extra inputs to give to useEffect.

The approach is naive but quite effective, we could complexify it a bit by accepting a tuple with an extra function given the mutation result, so we have control over the generated input given to useEffect :

useQuery(GET_TODO, { id }, { refetch: [['removeTodo', ({ id: removedId }) => id === removedId]] })

Add end to end tests

Package

All packages

Description

Add end to end test using one of the example apps. We've been using testcafe on other projects with some success, it should be fairly straight forward to set that up.

It would be great if we could, as part of a PR, build and deploy an example app and then run the tests against it.

If we used the next.js example(would require bringing in to this repo), then we could use now.sh and it's github pull request feature. The other option is using Heroku's review apps with any of our examples.

Console messages are nested

Environment

  • graphql-hooks version: 3.2.1
  • react version: 16.8.3
  • Browser: Safari & Chrome

Description

Second errors are nested within the first error on refetch.

screenshots are Safari & Chrome

screenshot 2019-03-04 at 11 16 58

screenshot 2019-03-04 at 11 18 22

How to reproduce

Something which fails, e.g. https://codesandbox.io/s/42k89kzpp7 (use expanded form to get the full console which has the error)

Suggested solution (optional)

Error should be somewhere here, but maybe just nested groups aren't possible?

console.groupCollapsed('---> Full Error Details');

useQuery only resends the query if options.variables change

Environment

  • graphql-hooks version: 3.1.0
  • react version: 16.8.3
  • Browser:

Description

The when calling useQuery(query, options) with different options for example operationName or fetchOptions it does not re-send the query.

How to reproduce

const QUERY = `
  query Operation1 {
    hello
  }

  query Operation2 {
    goodbye
  }
`

// example component that might change operationName based on prop
function MyComponent({ operationName }) {
  const { loading, error, data } = useQuery(QUERY, { options: { operationName } })
}

Suggested solution (optional)

// current
React.useEffect(() => {
    queryReq();
  }, [query, JSON.stringify(opts.variables)]);

// fix
React.useEffect(() => {
    queryReq();
  }, [query, JSON.stringify(opts)]);

useManualQuery does not use the cache

useManualQuery should be like a normal query and use the cache (if available). Currently the user would have to pass useCache=true for it to work as expected.

const [myQuery] = useManualQuery(MY_QUERY, { useCache: true })

Add an example for graphql upload mutation example.

I'm hoping it's possible to upload file to a server using graphql-hooks (form-data). If it's not how are you guys achieving file upload with graphql?
Can anyone add an example of upload mutation with graphql-hooks? Thanks

Consider using lerna

It might be worth using lerna for this project. Given that we have this repo, graphql-hooks-ssr and graphql-hooks-memcache already. I expect that to grow in future so may be worth setting up a monorepo soon.

Integration test for SSR

Package

graphql-hooks-ssr

Description

Add an integration test for the SSR package, making use useQuery etc.

Add support for Subscriptions

Description

We support Queries and Mutations but don't support the third operation type, Subscriptions. We should look at adding that.

Suggested implementation

I would guess websockets?

Inconsistent results for changing query

Package

graphql-hooks

Environment

  • graphql-hooks version: 3.4.0
  • react version: 16.8.4
  • Browser: Any

Description

When a query changes quickly results are inconsistent. What's wrong is that the current query is not tracked inside useClientRequest.js resulting in a race between different queries and the last one will be stored in state. In my case having consistent timing of server that means if I click next multiple times, I will see loading state at first and then multiple UI updates. Also, the loading state is lost since there's some of the old data in the state which arrived after all loading actions have been dispatched.

How to reproduce

I created a simple code sandbox where you can click next/prev multiple times and see the issue in action.
https://codesandbox.io/s/mj6m8yvv4p

Suggested solution (optional)

If this is undesired behavior I would like to open a PR solving that.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.