graphql-hooks
Advanced tools
Comparing version 2.0.0 to 2.0.1
{ | ||
"name": "graphql-hooks", | ||
"version": "2.0.0", | ||
"version": "2.0.1", | ||
"description": "Graphql Hooks", | ||
@@ -33,6 +33,7 @@ "main": "src/index.js", | ||
"jest": "24.1.0", | ||
"jest-fetch-mock": "2.1.1", | ||
"prettier": "1.16.4", | ||
"pretty-quick": "1.10.0", | ||
"react": "16.8.1", | ||
"react-dom": "16.8.1", | ||
"react": "16.8.2", | ||
"react-dom": "16.8.2", | ||
"react-testing-library": "5.8.0" | ||
@@ -49,2 +50,6 @@ }, | ||
"jest": { | ||
"automock": false, | ||
"setupFiles": [ | ||
"./test/setup.js" | ||
], | ||
"setupFilesAfterEnv": [ | ||
@@ -51,0 +56,0 @@ "react-testing-library/cleanup-after-each" |
237
README.md
@@ -5,4 +5,6 @@ # graphql-hooks | ||
[](https://coveralls.io/github/nearform/graphql-hooks?branch=master) | ||
 | ||
[](https://badge.fury.io/js/graphql-hooks) | ||
🎣 Minimal hooks-first graphql client. | ||
🎣 Minimal hooks-first GraphQL client. | ||
@@ -12,5 +14,6 @@ ## Features | ||
- 🥇 First-class hooks API | ||
- ⚖️ _Tiny_ bundle: only 3.7kB (1.4 gzipped) | ||
- 📄 Full SSR support: see [graphql-hooks-ssr](https://github.com/nearform/graphql-hooks-ssr) | ||
- 🔌 Plugin Caching: see [graphql-hooks-memcache](https://github.com/nearform/graphql-hooks-memcache) | ||
- 🔥 No more render props hell | ||
- ⚖️ Lightweight; only what you really need | ||
- ️️♻️ Promise-based API (works with `async` / `await`) | ||
- ⏳ Handle loading and error states with ease | ||
@@ -78,8 +81,7 @@ | ||
## TOC | ||
# Table of Contents | ||
- APIs | ||
- API | ||
- [GraphQLClient](#GraphQLClient) | ||
- [ClientContext](#ClientContext) | ||
- [useClient](#useClient) | ||
- [useQuery](#useQuery) | ||
@@ -91,19 +93,218 @@ - [useManualQuery](#useManualQuery) | ||
- [Authentication](#Authentication) | ||
- [Refetching a query](#Refetching-a-query) | ||
- [Fragments](#Fragments) | ||
- [Migrating from Apollo](#Migrating-from-Apollo) | ||
## API | ||
### `GraphQLClient` | ||
## `GraphQLClient` | ||
### `ClientContext` | ||
**Usage**: | ||
### `useClient` | ||
```js | ||
import { GraphQLClient } from 'graphql-hooks'; | ||
const client = new GraphQLClient(config); | ||
``` | ||
### `useQuery` | ||
**`config`**: Object containing configuration properties | ||
### `useManualQuery` | ||
- `url` (**Required**): The url to your GraphQL server | ||
- `ssrMode`: Boolean - set to `true` when using on the server for server-side rendering; defaults to `false` | ||
- `cache`: Object with the following methods: | ||
- `cache.get(key)` | ||
- `cache.set(key, data)` | ||
- `cache.delete(key)` | ||
- `cache.clear()` | ||
- `cache.keys()` | ||
- `getInitialState()` | ||
- See [graphql-hooks-memcache](https://github.com/nearform/graphql-hooks-memcache) as a reference implementation | ||
- `fetch(url, options)`: Fetch implementation - defaults to the global `fetch` API | ||
- `fetchOptions`: See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) for info on what options can be passed | ||
- `headers`: Object, e.g. `{ 'My-Header': 'hello' }` | ||
- `logErrors`: Boolean - defaults to `true` | ||
- `onError({ operation, result })`: Custom error handler | ||
- `operation`: Object with `query`, `variables` and `operationName` | ||
- `result`: Object containing `error`, `data`, `fetchError`, `httpError` and `graphqlErrors` | ||
### `useMutation` | ||
### `client` methods | ||
- `client.setHeader(key, value)`: Updates `client.headers` adding the new header to the existing headers | ||
- `client.setHeaders(headers)`: Replaces `client.headers` | ||
- `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)` | ||
## `ClientContext` | ||
`ClientContext` is the result of `React.createContext()` - meaning it can be used directly with React's new context API: | ||
**Example**: | ||
```js | ||
import { ClientContext } from 'graphql-hooks'; | ||
<ClientContext.Provider value={client}> | ||
{/* children can now consume the client context */} | ||
</ClientContext.Provider>; | ||
``` | ||
To access the `GraphQLClient` instance, call `React.useContext(ClientContext)`: | ||
```js | ||
import React, { useContext } from 'react'; | ||
import { ClientContext } from 'graphql-hooks'; | ||
function MyComponent() { | ||
const client = useContext(ClientContext); | ||
} | ||
``` | ||
## `useQuery` | ||
**Usage**: | ||
```js | ||
const state = useQuery(query, [options]); | ||
``` | ||
**Example:** | ||
```js | ||
import { useQuery } from 'graphql-hooks'; | ||
function MyComponent() { | ||
const { loading, error, data } = useQuery(query); | ||
if (loading) return 'Loading...'; | ||
if (error) return 'Something bad happened'; | ||
return <div>{data.thing}</div>; | ||
} | ||
``` | ||
This is a custom hook that takes care of fetching your query and storing the result in the cache. It won't refetch the query unless `query` or `options.variables` changes. | ||
- `query`: Your GraphQL query as a plain string | ||
- `options`: Object with the following optional properties | ||
- `variables`: Object e.g. `{ limit: 10 }` | ||
- `operationName`: If your query has multiple operations, pass the name of the operation you wish to execute. | ||
- `useCache`: Boolean - defaults to `true`; cache the query result | ||
- `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](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) for info on what options can be passed | ||
### `useQuery` return value | ||
```js | ||
const { loading, error, data, refetch, cacheHit, ...errors } = useQuery(QUERY); | ||
``` | ||
- `loading`: Boolean - `true` if the query is in flight | ||
- `error`: Boolean - `true` if `fetchError` or `httpError` or `graphQLErrors` has been set | ||
- `data`: Object - the result of your GraphQL query | ||
- `refetch`: Function - useful when refetching the same query after a mutation; NOTE this presets `skipCache=true` | ||
- `cacheHit`: Boolean - `true` if the query result came from the cache, useful for debugging | ||
- `fetchError`: Object - Set if an error occured during the `fetch` call | ||
- `httpError`: Object - Set if an error response was returned from the server | ||
- `graphQLErrors`: Array - Populated if any errors occured whilst resolving the query | ||
## `useManualQuery` | ||
Use this when you don't want a query to automactially be fetched, or wish to call a query programmatically. | ||
**Usage**: | ||
```js | ||
const [queryFn, state] = useManualQuery(query, [options]); | ||
``` | ||
**Example**: | ||
```js | ||
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: | ||
```js | ||
import { useManualQuery } from 'graphql-hooks'; | ||
function MyComponent(props) { | ||
const [fetchUser] = useManualQuery(GET_USER_QUERY); | ||
const fetchUserThenSomething = async () => { | ||
const user = await fetchUser({ | ||
variables: { id: props.userId } | ||
}); | ||
return somethingElse(); | ||
}; | ||
return ( | ||
<div> | ||
<button onClick={fetchUserThenSomething}>Get User!</button> | ||
</div> | ||
); | ||
} | ||
``` | ||
## `useMutation` | ||
Mutations unlike Queries are not cached. | ||
**Usage**: | ||
```js | ||
const [mutationFn, state] = useMutation(mutation, [options]); | ||
``` | ||
**Example**: | ||
```js | ||
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](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) for info on what options can be passed | ||
## Guides | ||
@@ -113,6 +314,14 @@ | ||
See [graphql-hooks-ssr](https://github.com/nearform/graphql-hooks-ssr) for an in depth guide. | ||
### Authentication | ||
### Refetching a query | ||
Coming soon! | ||
### Fragments | ||
Coming soon! | ||
### Migrating from Apollo | ||
Coming soon! |
class GraphQLClient { | ||
constructor(config) { | ||
constructor(config = {}) { | ||
// validate config | ||
if (!config.url) { | ||
throw new Error('GraphQLClient: config.url is required'); | ||
} | ||
if (config.fetch && typeof config.fetch !== 'function') { | ||
@@ -92,3 +96,3 @@ throw new Error('GraphQLClient: config.fetch must be a function'); | ||
async request(operation, options) { | ||
async request(operation, options = {}) { | ||
let result; | ||
@@ -95,0 +99,0 @@ |
@@ -0,5 +1,301 @@ | ||
import fetchMock from 'jest-fetch-mock'; | ||
import { GraphQLClient } from '../../src'; | ||
const validConfig = { | ||
url: 'https://my.graphql.api' | ||
}; | ||
const TEST_QUERY = `query Test($limit: Int) { | ||
tests(limit: $limit) { | ||
id | ||
} | ||
}`; | ||
describe('GraphQLClient', () => { | ||
it('runs a test', () => {}); | ||
describe('when instantiated', () => { | ||
it('throws if no url provided', () => { | ||
expect(() => { | ||
new GraphQLClient(); | ||
}).toThrow('GraphQLClient: config.url is required'); | ||
}); | ||
it('throws if fetch is not a function', () => { | ||
expect(() => { | ||
new GraphQLClient({ ...validConfig, fetch: 'fetch!' }); | ||
}).toThrow('GraphQLClient: config.fetch must be a function'); | ||
}); | ||
it('throws if fetch is not present or polyfilled', () => { | ||
const oldFetch = global.fetch; | ||
global.fetch = null; | ||
expect(() => { | ||
new GraphQLClient(validConfig); | ||
}).toThrow( | ||
'GraphQLClient: fetch must be polyfilled or passed in new GraphQLClient({ fetch })' | ||
); | ||
global.fetch = oldFetch; | ||
}); | ||
it('assigns config.cache to an instance property', () => { | ||
const cache = { get: 'get', set: 'set' }; | ||
const client = new GraphQLClient({ ...validConfig, cache }); | ||
expect(client.cache).toBe(cache); | ||
}); | ||
it('assigns config.headers to an instance property', () => { | ||
const headers = { 'My-Header': 'hello' }; | ||
const client = new GraphQLClient({ ...validConfig, headers }); | ||
expect(client.headers).toBe(headers); | ||
}); | ||
it('assigns config.ssrMode to an instance property', () => { | ||
const client = new GraphQLClient({ ...validConfig, ssrMode: true }); | ||
expect(client.ssrMode).toBe(true); | ||
}); | ||
it('assigns config.url to an instance property', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
expect(client.url).toBe(validConfig.url); | ||
}); | ||
it('assigns config.fetch to an instance property', () => { | ||
const myFetch = jest.fn(); | ||
const client = new GraphQLClient({ ...validConfig, fetch: myFetch }); | ||
expect(client.fetch).toBe(myFetch); | ||
}); | ||
it('assigns config.fetchOptions to an instance property', () => { | ||
const fetchOptions = { fetch: 'options' }; | ||
const client = new GraphQLClient({ ...validConfig, fetchOptions }); | ||
expect(client.fetchOptions).toBe(fetchOptions); | ||
}); | ||
it('assigns config.logErrors to an instance property', () => { | ||
const client = new GraphQLClient({ ...validConfig, logErrors: true }); | ||
expect(client.logErrors).toBe(true); | ||
}); | ||
it('assigns config.onError to an instance property', () => { | ||
const onError = jest.fn(); | ||
const client = new GraphQLClient({ ...validConfig, onError }); | ||
expect(client.onError).toBe(onError); | ||
}); | ||
}); | ||
describe('setHeader', () => { | ||
it('sets the key to the value', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.setHeader('My-Header', 'hello'); | ||
expect(client.headers['My-Header']).toBe('hello'); | ||
}); | ||
}); | ||
describe('setHeaders', () => { | ||
it('replaces all headers', () => { | ||
const headers = { 'My-Header': 'hello ' }; | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.setHeaders(headers); | ||
expect(client.headers).toBe(headers); | ||
}); | ||
}); | ||
describe('logErrorResult', () => { | ||
let logSpy, errorSpy, groupCollapsedSpy, groupEndSpy; | ||
beforeEach(() => { | ||
logSpy = spyOn(global.console, 'log'); | ||
errorSpy = spyOn(global.console, 'error'); | ||
groupCollapsedSpy = spyOn(global.console, 'groupCollapsed'); | ||
groupEndSpy = spyOn(global.console, 'groupEnd'); | ||
}); | ||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
it('calls onError if present', () => { | ||
const onError = jest.fn(); | ||
const client = new GraphQLClient({ ...validConfig, onError }); | ||
client.logErrorResult({ result: 'result', operation: 'operation' }); | ||
expect(onError).toHaveBeenCalledWith({ | ||
result: 'result', | ||
operation: 'operation' | ||
}); | ||
}); | ||
it('logs a fetchError', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.logErrorResult({ result: { fetchError: 'on no fetch!' } }); | ||
expect(groupCollapsedSpy).toHaveBeenCalledWith('FETCH ERROR:'); | ||
expect(logSpy).toHaveBeenCalledWith('on no fetch!'); | ||
expect(groupEndSpy).toHaveBeenCalled(); | ||
}); | ||
it('logs an httpError', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.logErrorResult({ result: { httpError: 'on no http!' } }); | ||
expect(groupCollapsedSpy).toHaveBeenCalledWith('HTTP ERROR:'); | ||
expect(logSpy).toHaveBeenCalledWith('on no http!'); | ||
expect(groupEndSpy).toHaveBeenCalled(); | ||
}); | ||
it('logs all graphQLErrors', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
const graphQLErrors = ['on no GraphQL!', 'oops GraphQL!']; | ||
client.logErrorResult({ result: { graphQLErrors } }); | ||
expect(groupCollapsedSpy).toHaveBeenCalledWith('GRAPHQL ERROR:'); | ||
expect(logSpy).toHaveBeenCalledWith('on no GraphQL!'); | ||
expect(logSpy).toHaveBeenCalledWith('oops GraphQL!'); | ||
expect(groupEndSpy).toHaveBeenCalled(); | ||
}); | ||
}); | ||
describe('generateResult', () => { | ||
it('shows as errored if there are graphQL errors', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
const result = client.generateResult({ | ||
graphQLErrors: ['error 1', 'error 2'] | ||
}); | ||
expect(result.error).toBe(true); | ||
}); | ||
it('shows as errored if there is a fetch error', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
const result = client.generateResult({ | ||
fetchError: 'fetch error' | ||
}); | ||
expect(result.error).toBe(true); | ||
}); | ||
it('shows as errored if there is an http error', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
const result = client.generateResult({ | ||
httpError: 'http error' | ||
}); | ||
expect(result.error).toBe(true); | ||
}); | ||
it('returns the errors & data', () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
const data = { | ||
graphQLErrors: ['graphQL error 1', 'graphQL error 2'], | ||
fetchError: 'fetch error', | ||
httpError: 'http error', | ||
data: 'data!' | ||
}; | ||
const result = client.generateResult(data); | ||
expect(result).toEqual({ | ||
error: true, | ||
graphQLErrors: data.graphQLErrors, | ||
fetchError: data.fetchError, | ||
httpError: data.httpError, | ||
data: data.data | ||
}); | ||
}); | ||
}); | ||
describe('getCacheKey', () => { | ||
it('returns a cache key', () => { | ||
const client = new GraphQLClient({ | ||
...validConfig, | ||
fetchOptions: { optionOne: 1 } | ||
}); | ||
const cacheKey = client.getCacheKey('operation', { | ||
fetchOptionsOverrides: { optionTwo: 2 } | ||
}); | ||
expect(cacheKey).toEqual({ | ||
operation: 'operation', | ||
fetchOptions: { optionOne: 1, optionTwo: 2 } | ||
}); | ||
}); | ||
}); | ||
describe('request', () => { | ||
afterEach(() => { | ||
fetch.resetMocks(); | ||
}); | ||
it('sends the request to the configured url', async () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
fetch.mockResponseOnce(JSON.stringify({ data: 'data' })); | ||
await client.request({ query: TEST_QUERY }); | ||
const actual = fetch.mock.calls[0][0]; | ||
const expected = validConfig.url; | ||
expect(actual).toBe(expected); | ||
}); | ||
it('applies the configured headers', async () => { | ||
const headers = { 'My-Header': 'hello' }; | ||
const client = new GraphQLClient({ ...validConfig, headers }); | ||
fetch.mockResponseOnce(JSON.stringify({ data: 'data' })); | ||
await client.request({ query: TEST_QUERY }); | ||
const actual = fetch.mock.calls[0][1].headers['My-Header']; | ||
const expected = 'hello'; | ||
expect(actual).toBe(expected); | ||
}); | ||
it('sends the provided operation query, variables and name', async () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
fetch.mockResponseOnce(JSON.stringify({ data: 'data' })); | ||
const operation = { | ||
query: TEST_QUERY, | ||
variables: { limit: 1 }, | ||
operationName: 'test' | ||
}; | ||
await client.request(operation); | ||
const actual = fetch.mock.calls[0][1].body; | ||
const expected = JSON.stringify(operation); | ||
expect(actual).toBe(expected); | ||
}); | ||
it('handles & returns fetch errors', async () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.logErrorResult = jest.fn(); | ||
const error = new Error('Oops fetch!'); | ||
fetch.mockRejectOnce(error); | ||
const res = await client.request({ query: TEST_QUERY }); | ||
expect(res.fetchError).toBe(error); | ||
}); | ||
it('handles & returns http errors', async () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.logErrorResult = jest.fn(); | ||
fetch.mockResponseOnce('Denied!', { | ||
status: 403 | ||
}); | ||
const res = await client.request({ query: TEST_QUERY }); | ||
expect(res.httpError).toEqual({ | ||
status: 403, | ||
statusText: 'Forbidden', | ||
body: 'Denied!' | ||
}); | ||
}); | ||
it('returns valid responses', async () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
fetch.mockResponseOnce(JSON.stringify({ data: 'data!' })); | ||
const res = await client.request({ query: TEST_QUERY }); | ||
expect(res.data).toBe('data!'); | ||
}); | ||
it('returns graphql errors', async () => { | ||
const client = new GraphQLClient({ ...validConfig }); | ||
client.logErrorResult = jest.fn(); | ||
fetch.mockResponseOnce( | ||
JSON.stringify({ data: 'data!', errors: ['oops!'] }) | ||
); | ||
const res = await client.request({ query: TEST_QUERY }); | ||
expect(res.graphQLErrors).toEqual(['oops!']); | ||
}); | ||
it('will use a configured fetch implementation', async () => { | ||
fetchMock.mockResponseOnce(JSON.stringify({ data: 'data' })); | ||
const client = new GraphQLClient({ ...validConfig, fetch: fetchMock }); | ||
await client.request({ query: TEST_QUERY }); | ||
expect(fetchMock).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
36391
18
553
323
11
13