Skip to content

feat: refetch query when subscribed mutation executes #686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Nov 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ This is a custom hook that takes care of fetching your query and storing the res
- `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
- `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

Expand Down Expand Up @@ -574,6 +580,50 @@ export default function PostList() {
}
```

## Refetch queries with mutations subscription

We can have a query to automatically refetch when any mutation from a provided list execute.
In the following example we are refetching a list of posts for a given user.

**Example**

```jsx
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
}
]
})
```


## File uploads

`graphql-hooks` complies with the [GraphQL multipart request spec](https://github.com/jaydenseric/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](https://www.apollographql.com/docs/apollo-server) (see list of supported servers [here](https://github.com/jaydenseric/graphql-multipart-request-spec#server)).
Expand Down
17 changes: 5 additions & 12 deletions examples/create-react-app/src/components/CreatePost.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
import { useMutation } from 'graphql-hooks'
import T from 'prop-types'
import React from 'react'
import CreatePostForm from './CreatePostForm'

const createPostMutation = `
mutation CreatePost($id: ID!, $title: String!, $url: String!) {
createPost(id: $id, title: $title, url: $url) {
export const createPostMutation = `
mutation CreatePost($title: String!, $url: String!) {
createPost(title: $title, url: $url) {
id
}
}
`

export default function CreatePost({ onSuccess }) {
export default function CreatePost() {
const [createPost, { loading, error }] = useMutation(createPostMutation)

async function handleSubmit({ title, url }) {
const id = Math.floor(Math.random() * 1000000)
await createPost({ variables: { id, title, url } })
onSuccess && onSuccess()
await createPost({ variables: { title, url } })
}

return (
<CreatePostForm loading={loading} error={error} onSubmit={handleSubmit} />
)
}

CreatePost.propTypes = {
onSuccess: T.func
}
2 changes: 1 addition & 1 deletion examples/create-react-app/src/components/CreatePostForm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import T from 'prop-types'
import React, { useState } from 'react'

export default function CreatePostForm({ loading, error, onSubmit }) {
const [title, setTitle] = useState('')
Expand Down
6 changes: 4 additions & 2 deletions examples/create-react-app/src/components/Posts.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQuery } from 'graphql-hooks'
import T from 'prop-types'
import React from 'react'
import CreatePost from './CreatePost'
import CreatePost, { createPostMutation } from './CreatePost'

export const allPostsQuery = `
query {
Expand All @@ -14,7 +14,9 @@ export const allPostsQuery = `
`

export default function Posts() {
const { loading, data, error, refetch } = useQuery(allPostsQuery)
const { loading, data, error, refetch } = useQuery(allPostsQuery, {
refetchAfterMutations: createPostMutation
})

return (
<>
Expand Down
11 changes: 6 additions & 5 deletions examples/typescript/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const client = new GraphQLClient({

export const allPostsQuery = `
query {
allPosts(orderBy: createdAt_DESC, first: 20) {
allPosts {
id
title
url
Expand All @@ -46,15 +46,14 @@ const postQuery = `
}
`

function AddPost({ onSuccess }: { onSuccess: () => void }) {
function AddPost() {
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
const [createPost, { loading, error }] = useMutation(createPostMutation)

async function handleSubmit(e: any) {
e.preventDefault()
await createPost({ variables: { title, url } })
onSuccess && onSuccess()
}

return (
Expand Down Expand Up @@ -83,12 +82,14 @@ function AddPost({ onSuccess }: { onSuccess: () => void }) {
}

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

return (
<>
<h2>Add post</h2>
<AddPost onSuccess={refetch} />
<AddPost />
<h2>Posts</h2>
<button onClick={() => refetch()}>Reload</button>
<PostList loading={loading} error={error} data={data} />
Expand Down
22 changes: 19 additions & 3 deletions packages/graphql-hooks/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EventEmitter from 'events'
import { Client } from 'graphql-ws'
import * as React from 'react'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { Client } from 'graphql-ws'

// Exports

export class GraphQLClient {
Expand All @@ -14,6 +14,7 @@ export class GraphQLClient {
FormData?: any
logErrors: boolean
useGETForQueries: boolean
mutationsEmitter: EventEmitter

subscriptionClient?: SubscriptionClient | Client

Expand Down Expand Up @@ -171,7 +172,10 @@ interface Result<ResponseData = any, TGraphQLError = object> {
error?: APIError<TGraphQLError>
}

export interface UseClientRequestOptions<ResponseData = any, Variables = object> {
export interface UseClientRequestOptions<
ResponseData = any,
Variables = object
> {
useCache?: boolean
isMutation?: boolean
isManual?: boolean
Expand All @@ -183,10 +187,22 @@ export interface UseClientRequestOptions<ResponseData = any, Variables = object>
client?: GraphQLClient
}

type RefetchAfterMutationItem = {
mutation: string
filter?: (variables: object) => boolean
}

export type RefetchAferMutationsData =
| string
| string[]
| RefetchAfterMutationItem
| RefetchAfterMutationItem[]

export interface UseQueryOptions<ResponseData = any, Variables = object>
extends UseClientRequestOptions<ResponseData, Variables> {
ssr?: boolean
skip?: boolean
refetchAfterMutations?: RefetchAferMutationsData
}

interface UseClientRequestResult<ResponseData, TGraphQLError = object> {
Expand Down
78 changes: 78 additions & 0 deletions packages/graphql-hooks/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/graphql-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
},
"dependencies": {
"dequal": "^2.0.0",
"events": "^3.3.0",
"extract-files": "^11.0.0"
},
"devDependencies": {
Expand Down
5 changes: 3 additions & 2 deletions packages/graphql-hooks/src/GraphQLClient.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EventEmitter from 'events'
import { extractFiles } from 'extract-files'

import isExtractableFileEnhanced from './isExtractableFileEnhanced'
import canUseDOM from './canUseDOM'
import isExtractableFileEnhanced from './isExtractableFileEnhanced'

class GraphQLClient {
constructor(config = {}) {
Expand Down Expand Up @@ -39,6 +39,7 @@ class GraphQLClient {
this.onError = config.onError
this.useGETForQueries = config.useGETForQueries === true
this.subscriptionClient = config.subscriptionClient
this.mutationsEmitter = new EventEmitter()
}

setHeader(key, value) {
Expand Down
29 changes: 29 additions & 0 deletions packages/graphql-hooks/src/createRefetchMutationsMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Checks values of refetchAfterMutations public option and maps them to an object
* @typedef {import('../index').RefetchAferMutationsData} RefetchAferMutationsData
*
* @param {RefetchAferMutationsData} refetchAfterMutations
* @returns {object}
*/
export default function createRefetchMutationsMap(refetchAfterMutations) {
const mutations = Array.isArray(refetchAfterMutations)
? refetchAfterMutations
: [refetchAfterMutations]
const result = {}

mutations.forEach(mutationInfo => {
if (mutationInfo == null) return

const paramType = typeof mutationInfo

if (paramType === 'string') {
result[mutationInfo] = {}
} else if (paramType === 'object') {
const { filter, mutation } = mutationInfo

result[mutation] = { filter }
}
})

return result
}
Loading