graphql-hooks
Advanced tools
Comparing version 3.0.0 to 3.1.0
@@ -38,4 +38,13 @@ # Welcome to GraphQL Hooks! | ||
Release process: | ||
## Updating Contributors list in README.md | ||
You can add yourself or another contributor by either: | ||
- Comment on an issue or pull request `@all-contributors please add <username> for <contributions>` | ||
- `npm run contributors:add` | ||
For more information on `@all-contributors` see it's [usage docs](https://allcontributors.org/docs/en/bot/usage) | ||
### Release process: | ||
- `npm test` | ||
@@ -42,0 +51,0 @@ - `npm version <major|minor|patch>` |
{ | ||
"name": "graphql-hooks", | ||
"version": "3.0.0", | ||
"version": "3.1.0", | ||
"description": "Graphql Hooks", | ||
@@ -10,3 +10,5 @@ "main": "src/index.js", | ||
"test:coverage": "jest --coverage", | ||
"prettier": "pretty-quick" | ||
"prettier": "pretty-quick", | ||
"contributors:add": "all-contributors add", | ||
"contributors:generate": "all-contributors generate" | ||
}, | ||
@@ -31,2 +33,3 @@ "keywords": [ | ||
"@babel/preset-react": "7.0.0", | ||
"all-contributors-cli": "6.1.2", | ||
"babel-jest": "24.1.0", | ||
@@ -33,0 +36,0 @@ "husky": "1.3.1", |
143
README.md
@@ -7,2 +7,3 @@ # graphql-hooks | ||
[](https://badge.fury.io/js/graphql-hooks) | ||
[](#contributors) | ||
@@ -90,2 +91,3 @@ 🎣 Minimal hooks-first GraphQL client. | ||
- [SSR](#SSR) | ||
- [Pagination](#Pagination) | ||
- [Authentication](#Authentication) | ||
@@ -192,2 +194,5 @@ - [Fragments](#Fragments) | ||
- `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 | ||
- `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 | ||
@@ -203,3 +208,4 @@ ### `useQuery` return value | ||
- `data`: Object - the result of your GraphQL query | ||
- `refetch`: Function - useful when refetching the same query after a mutation; NOTE this presets `skipCache=true` | ||
- `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 | ||
@@ -316,2 +322,126 @@ - `fetchError`: Object - Set if an error occured during the `fetch` call | ||
### Pagination | ||
[GraphQL Pagination](https://graphql.org/learn/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: | ||
```javascript | ||
export const allPostsQuery = ` | ||
query allPosts($first: Int!, $skip: Int!) { | ||
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) { | ||
id | ||
title | ||
votes | ||
url | ||
createdAt | ||
} | ||
_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: | ||
```jsx | ||
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: | ||
```jsx | ||
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> | ||
); | ||
} | ||
``` | ||
### Authentication | ||
@@ -328,1 +458,12 @@ | ||
Coming soon! | ||
## Contributors | ||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): | ||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> | ||
<!-- prettier-ignore --> | ||
<table><tr><td align="center"><a href="https://twitter.com/bmullan91"><img src="https://avatars1.githubusercontent.com/u/1939483?v=4" width="100px;" alt="Brian Mullan"/><br /><sub><b>Brian Mullan</b></sub></a><br /><a href="#question-bmullan91" title="Answering Questions">💬</a> <a href="https://github.com/nearfrorm/graphql-hooks/issues?q=author%3Abmullan91" title="Bug reports">🐛</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=bmullan91" title="Code">💻</a> <a href="#content-bmullan91" title="Content">🖋</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=bmullan91" title="Documentation">📖</a> <a href="#example-bmullan91" title="Examples">💡</a> <a href="#ideas-bmullan91" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-bmullan91" title="Maintenance">🚧</a> <a href="#review-bmullan91" title="Reviewed Pull Requests">👀</a></td><td align="center"><a href="https://jackdc.com"><img src="https://avatars0.githubusercontent.com/u/1485654?v=4" width="100px;" alt="Jack Clark"/><br /><sub><b>Jack Clark</b></sub></a><br /><a href="#question-jackdclark" title="Answering Questions">💬</a> <a href="https://github.com/nearfrorm/graphql-hooks/issues?q=author%3Ajackdclark" title="Bug reports">🐛</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=jackdclark" title="Code">💻</a> <a href="#content-jackdclark" title="Content">🖋</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=jackdclark" title="Documentation">📖</a> <a href="#example-jackdclark" title="Examples">💡</a> <a href="#ideas-jackdclark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-jackdclark" title="Maintenance">🚧</a> <a href="#review-jackdclark" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=jackdclark" title="Tests">⚠️</a></td><td align="center"><a href="http://twitter.com/joezo"><img src="https://avatars1.githubusercontent.com/u/2870255?v=4" width="100px;" alt="Joe Warren"/><br /><sub><b>Joe Warren</b></sub></a><br /><a href="#question-Joezo" title="Answering Questions">💬</a> <a href="https://github.com/nearfrorm/graphql-hooks/issues?q=author%3AJoezo" title="Bug reports">🐛</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=Joezo" title="Code">💻</a> <a href="#content-Joezo" title="Content">🖋</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=Joezo" title="Documentation">📖</a> <a href="#example-Joezo" title="Examples">💡</a> <a href="#ideas-Joezo" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-Joezo" title="Maintenance">🚧</a> <a href="#review-Joezo" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=Joezo" title="Tests">⚠️</a></td><td align="center"><a href="http://simoneb.github.io"><img src="https://avatars1.githubusercontent.com/u/20181?v=4" width="100px;" alt="Simone Busoli"/><br /><sub><b>Simone Busoli</b></sub></a><br /><a href="#question-simoneb" title="Answering Questions">💬</a> <a href="https://github.com/nearfrorm/graphql-hooks/issues?q=author%3Asimoneb" title="Bug reports">🐛</a> <a href="https://github.com/nearfrorm/graphql-hooks/commits?author=simoneb" title="Documentation">📖</a></td></tr></table> | ||
<!-- ALL-CONTRIBUTORS-LIST:END --> | ||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! |
@@ -91,4 +91,11 @@ const React = require('react'); | ||
dispatch({ type: actionTypes.LOADING }); | ||
const result = await client.request(revisedOperation, revisedOpts); | ||
let result = await client.request(revisedOperation, revisedOpts); | ||
if (state.data && result.data && revisedOpts.updateData) { | ||
if (typeof revisedOpts.updateData !== 'function') { | ||
throw new Error('options.updateData must be a function'); | ||
} | ||
result.data = revisedOpts.updateData(state.data, result.data); | ||
} | ||
if (revisedOpts.useCache && client.cache) { | ||
@@ -95,0 +102,0 @@ client.cache.set(revisedCacheKey, result); |
@@ -11,8 +11,6 @@ const React = require('react'); | ||
module.exports = function useQuery(query, opts = {}) { | ||
const allOpts = { ...defaultOpts, ...opts }; | ||
const client = React.useContext(ClientContext); | ||
const [calledDuringSSR, setCalledDuringSSR] = React.useState(false); | ||
const [queryReq, state] = useClientRequest(query, { | ||
...defaultOpts, | ||
...opts | ||
}); | ||
const [queryReq, state] = useClientRequest(query, allOpts); | ||
@@ -34,4 +32,12 @@ if (client.ssrMode && opts.ssr !== false && !calledDuringSSR) { | ||
...state, | ||
refetch: () => queryReq({ skipCache: true }) | ||
refetch: (options = {}) => | ||
queryReq({ | ||
skipCache: true, | ||
// don't call the updateData that has been passed into useQuery here | ||
// reset to the default behaviour of returning the raw query result | ||
// this can be overridden in refetch options | ||
updateData: (_, data) => data, | ||
...options | ||
}) | ||
}; | ||
}; |
@@ -27,3 +27,3 @@ import React from 'react'; | ||
}, | ||
request: jest.fn().mockResolvedValue({ some: 'data' }) | ||
request: jest.fn().mockResolvedValue({ data: 'data' }) | ||
}; | ||
@@ -131,3 +131,3 @@ }); | ||
); | ||
expect(state).toEqual({ cacheHit: false, loading: false, some: 'data' }); | ||
expect(state).toEqual({ cacheHit: false, loading: false, data: 'data' }); | ||
}); | ||
@@ -186,3 +186,3 @@ | ||
loading: false, | ||
some: 'data' | ||
data: 'data' | ||
}); | ||
@@ -204,3 +204,3 @@ }); | ||
loading: false, | ||
some: 'data' | ||
data: 'data' | ||
}); | ||
@@ -219,6 +219,90 @@ }); | ||
expect(mockClient.cache.set).toHaveBeenCalledWith('cacheKey', { | ||
some: 'data' | ||
data: 'data' | ||
}); | ||
}); | ||
describe('options.updateRequest', () => { | ||
it('is called with old & new data if the data has changed & the result is returned', async () => { | ||
let fetchData, state; | ||
const updateDataMock = jest.fn().mockReturnValue('merged data'); | ||
testHook( | ||
() => | ||
([fetchData, state] = useClientRequest(TEST_QUERY, { | ||
variables: { limit: 10 }, | ||
updateData: updateDataMock | ||
})), | ||
{ wrapper: Wrapper } | ||
); | ||
// first fetch to populate state | ||
await fetchData(); | ||
mockClient.request.mockResolvedValueOnce({ data: 'new data' }); | ||
await fetchData({ variables: { limit: 20 } }); | ||
expect(updateDataMock).toHaveBeenCalledWith('data', 'new data'); | ||
expect(state).toEqual({ | ||
cacheHit: false, | ||
data: 'merged data', | ||
loading: false | ||
}); | ||
}); | ||
it('is not called if there is no old data', async () => { | ||
let fetchData; | ||
const updateDataMock = jest.fn(); | ||
testHook( | ||
() => | ||
([fetchData] = useClientRequest(TEST_QUERY, { | ||
variables: { limit: 10 }, | ||
updateData: updateDataMock | ||
})), | ||
{ wrapper: Wrapper } | ||
); | ||
await fetchData(); | ||
expect(updateDataMock).not.toHaveBeenCalled(); | ||
}); | ||
it('is not called if there is no new data', async () => { | ||
let fetchData; | ||
const updateDataMock = jest.fn(); | ||
testHook( | ||
() => | ||
([fetchData] = useClientRequest(TEST_QUERY, { | ||
variables: { limit: 10 }, | ||
updateData: updateDataMock | ||
})), | ||
{ wrapper: Wrapper } | ||
); | ||
await fetchData(); | ||
mockClient.request.mockReturnValueOnce({ errors: ['on no!'] }); | ||
await fetchData({ variables: { limit: 20 } }); | ||
expect(updateDataMock).not.toHaveBeenCalled(); | ||
}); | ||
it('throws if updateData is not a function', async () => { | ||
let fetchData; | ||
testHook( | ||
() => | ||
([fetchData] = useClientRequest(TEST_QUERY, { | ||
variables: { limit: 10 }, | ||
updateData: 'do I look like a function to you?' | ||
})), | ||
{ wrapper: Wrapper } | ||
); | ||
// first fetch to populate state | ||
await fetchData(); | ||
expect(fetchData({ variables: { limit: 20 } })).rejects.toThrow( | ||
'options.updateData must be a function' | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -56,3 +56,3 @@ import React from 'react'; | ||
it('returns initial state from useClientRequest & refetch', () => { | ||
it('returns initial state from useClientRequest, refetch & fetchMore', () => { | ||
let state; | ||
@@ -71,5 +71,51 @@ testHook(() => (state = useQuery(TEST_QUERY)), { wrapper: Wrapper }); | ||
refetch(); | ||
expect(mockQueryReq).toHaveBeenCalledWith({ skipCache: true }); | ||
expect(mockQueryReq).toHaveBeenCalledWith({ | ||
skipCache: true, | ||
updateData: expect.any(Function) | ||
}); | ||
}); | ||
it('merges options when refetch is called', () => { | ||
let refetch; | ||
testHook( | ||
() => | ||
({ refetch } = useQuery(TEST_QUERY, { | ||
variables: { skip: 0, first: 10 } | ||
})), | ||
{ | ||
wrapper: Wrapper | ||
} | ||
); | ||
const updateData = () => {}; | ||
refetch({ | ||
extra: 'option', | ||
variables: { skip: 10, first: 10, extra: 'variable' }, | ||
updateData | ||
}); | ||
expect(mockQueryReq).toHaveBeenCalledWith({ | ||
skipCache: true, | ||
extra: 'option', | ||
variables: { skip: 10, first: 10, extra: 'variable' }, | ||
updateData | ||
}); | ||
}); | ||
it('gets updateData to replace the result by default', () => { | ||
let refetch; | ||
testHook( | ||
() => | ||
({ refetch } = useQuery(TEST_QUERY, { | ||
variables: { skip: 0, first: 10 } | ||
})), | ||
{ | ||
wrapper: Wrapper | ||
} | ||
); | ||
mockQueryReq.mockImplementationOnce(({ updateData }) => { | ||
return updateData('previousData', 'data'); | ||
}); | ||
refetch(); | ||
expect(mockQueryReq).toHaveReturnedWith('data'); | ||
}); | ||
it('sends the query on mount if no data & no error', () => { | ||
@@ -76,0 +122,0 @@ testHook(() => useQuery(TEST_QUERY), { wrapper: Wrapper }); |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
63842
21
1053
464
13
12