graphql-hooks
graphql-hooks copied to clipboard
Manually update the cache after a mutation or a subscription
I'm not sure if such thing is possible right now. There's nothing on the docs about it.
I found this issue, on where an updateCache
method is proposed, but seems it has not been implemented.
https://github.com/nearform/graphql-hooks/issues/52
If I understand what you mean this was done in https://github.com/nearform/graphql-hooks/pull/686. If so, please close this issue.
No, I mean manually update the cache, like in the proposed example:
import { LIST_USERS_QUERY } from "./somewhere";
const CREATE_USER_MUTATION = `...`;
function CreateNewUser() {
const [createUser] = useMutation(CREATE_USER_MUTATION, {
updateCache: (cache, result) => {
const cacheKey = client.getCacheKey({ query: LIST_USERS_QUERY });
const existingResult = cache.get(cacheKey);
cache.set(cacheKey, {
...existingResult,
data: {
users: [...existingResult.data.users, result.data.createUser]
}
});
}
});
}
As far as I understand, this is not possible right now?
As you can see in the source code this is not supported right now.
All the options were discussed in https://github.com/nearform/graphql-hooks/issues/52 and we decided to implement one of them in #686. They obtain similar results but in different ways.
We can currently handle this kind of scenarios using the mutationsEmitter
:
import { useContext, useEffect } from "react"
import { ClientContext } from "graphql-hooks"
import { CREATE_USER_MUTATION } from "./somewhere"
const LIST_USERS_QUERY = `...`
function Users() {
const client = useContext(ClientContext)
const { data, cacheKey, refetch } = useQuery(LIST_USERS_QUERY)
useEffect(() => {
if (!client.cache) return
const handler = ({ result }) => {
const existingResult = cache.get(cacheKey)
cache.set(cacheKey, {
...existingResult,
data: {
users: [...existingResult.data.users, result.data.createUser],
},
})
refetch({ skipCache: false })
}
client.mutationsEmitter.on(CREATE_USER_MUTATION, handler)
return () =>
client.mutationsEmitter.removeListener(CREATE_USER_MUTATION, handler)
})
}
We could build an hook on top of mutationsEmitter
to simplify it's usage (something like useMutationListener
)
With it we can do pretty much everything related to the cache management (I'm using it for invalidating without refetching)
Ohh it’s great to know we have this mechanism, thanks @gdorsi! Yeah I think it would be great to provide a simpler abstraction on top of this before making it public/official.
Interested to know more about this…
something like useMutationListener
I mean, I never used an hook for this use case. I only have experience with Apollo Client and React Query, but I think their API is good, even though it is imperative?
- https://www.apollographql.com/docs/react/caching/cache-interaction/#using-updatequery-and-updatefragment
- https://react-query.tanstack.com/reference/QueryClient#queryclientsetquerydata
I was thinking about this kind of API:
import { useContext, useEffect } from "react"
import { useMutationListener } from "graphql-hooks"
import { CREATE_USER_MUTATION } from "./somewhere"
const LIST_USERS_QUERY = `...`
function Users() {
const client = useContext(ClientContext)
const { data, cacheKey, refetch } = useQuery(LIST_USERS_QUERY)
useMutationListener(CREATE_USER_MUTATION, ({ result, cache }) => {
const existingResult = cache.get(cacheKey)
cache.set(cacheKey, {
...existingResult,
data: {
users: [...existingResult.data.users, result.data.createUser],
},
})
// This also need to be simplified in some way
refetch({ skipCache: false })
})
}
the impact for the codebase would be small and with proper documentation I think this API could be pretty usable.
EDIT:
In the example comments I said I'm using the cacheKey directly because the match here is by reference
that is wrong, because the key is serialized here
Uhm so it's like an inversion of control actually, I mean, usually you have:
useMutation + updateCache
But you're proposing:
useQuery + useMutationListener
After thinking about it... I think it's not gonna work.
The cache update must happen right after the mutation completes. The mutation is always fired from the actual running code, usually from a user action... even if the query corresponding to cacheKey is not mounted, its cache has been updated, so when it mounts it will have the correct state.
With the proposed approach you would be only able to "listen mutations" from mounted components... I mean, imagine that you have 2 separate pages (only 1 page will be mounted at a given time, not both):
- page 1 (list of items) - useQuery(GET_LIST)
- page 2 (item detail, edit, remove...) - useMutation(EDIT_ITEM)
The mutation will happen on page 2. But you can't listen to it from page 1 because it is unmounted -- hence not running.
Right?
The mutation will happen on page 2. But you can't listen to it from page 1
The key is generated taking the variables in account so in that case would be hard to update the cache correctly (you need to know in what page is located your resource)
Tweaking the cache is good when the the use case is simple (no variables or to do invalidation), not when things start to become complex.
In that case is better to switch to a state manager IMHO.
Anyway if we take as example an application where the CreateNewUser component is in a dedicated page that's where my approach, and the current invalidation system, falls short (you could store the key in a global state but the complexity would start to be too much).
In that case we can suggest to do this:
import { LIST_USERS_QUERY } from "./somewhere";
import { ClientContext } from "graphql-hooks";
import { useContext, useEffect } from "react";
import { CreateNewUserForm } from './somewhereElse' ;
const CREATE_USER_MUTATION = `...`;
function CreateNewUser() {
const client = useContext(ClientContext);
const updateCache = (result) => {
const cacheKey = client.getCacheKey({ query: LIST_USERS_QUERY });
const existingResult = client.cache.get(cacheKey);
cache.set(cacheKey, {
...existingResult,
data: {
users: [...existingResult.data.users, result.data.createUser],
},
});
};
const [createUser] = useMutation(CREATE_USER_MUTATION);
const handleUserCreation = (payload) => {
return createUser(payload).then(updateCache);
};
return <CreateNewUserForm onSubmit={handleUserCreation) />;
}
It's not clean as your proposal, but doesn't seem too complex to me.
WDYT?
EDIT: I've updated the example to show how handleUserCreation
could be used.
Well, the example above is fine! I mean, if right now we can already update the cache like that, I would maybe document it.
If we want to update the cache on subscriptions, can it be done in this same way?
If we want to update the cache on subscriptions, can it be done in this same way?
I've never used them, but until you can hook into some events I think is doable.
Hi there :) , there are some bits from the last @gdorsi's proposal that I'm missing.
I'm fairly new to React, and right now I fail to see how handleUserCreation
would be used as I don't see any reference to it after its instantiation, nor any other reference in the prior examples (in case this was a shortened version and the redundant aspects were already omitted).
I've never used them, but until you can hook into some events I think is doable.
@gdorsi Hopefully these examples could help. It's a really simple react app to test different server/clients against GraphQL operations; you can see how it manages to update the cache manually after a e.g. subscription:
Using graphql-hooks: https://github.com/nuragic/graphql-react-app/blob/graphql-hooks/src/App.js#L39-L52 Using React Query: https://github.com/nuragic/graphql-react-app/blob/main/src/App.js#L57-L72
handleUserCreation would be used as I don't see any reference to it after its instantiation, nor any other reference in the prior examples
@castarco Fair point, that's right... 😄 I guess it should be called later in the code instead of just calling createUser
... but it's more a question for @gdorsi
@castarco I've updated the example with the missing parts (I've also realized that I was missing the createUser
call XD)
@nuragic Thanks for the examples, very helpful! It should be pretty similar to the react-query implementation, I will open a PR to check if it really works with useSubscription
@gdorsi I asked @liana-pigeot to look into this, are you actually working on it?
Ok thanks!
are you actually working on it?
Nope
Hi folks, how are you? I just arrived at this issue and as homework already read the following related issues:
- https://github.com/nearform/graphql-hooks/issues/52
- https://github.com/nearform/graphql-hooks/issues/74
- https://github.com/nearform/graphql-hooks/pull/686
I'd like to make a new proposal to resolve all necessities (please, let me know if makes sense for you):
The first one will re-fetch all data after a mutation success by calling invalidateQuery
method.
import { useQueryClient, useMutation } from "graphql-hooks";
const SOME_MUTATION = `...`;
const SOME_QUERY = `...`;
const MyForm = () => {
const client = useQueryClient()
const [callMutation, { ... }] = useMutation(SOME_MUTATION, {
onSuccess: () => client.invalidateQuery(SOME_QUERY)
})
const handleFormSubmit = payload => callMutation(payload)
return (
<form onSubmit={handleFormSubmit}>
...
</form>
)
}
The second one will update the cache without making a new request to the API avoiding retrieving all data again by calling setQueryData
method:
import { useQueryClient, useMutation } from "graphql-hooks";
const SOME_MUTATION = `...`;
const SOME_QUERY = `...`;
const MyForm = () => {
const client = useQueryClient()
const [callMutation, { ... }] = useMutation(SOME_MUTATION, {
onSuccess: (result) => client.setQueryData(SOME_QUERY, (oldState) => [...oldState, result])
})
const handleFormSubmit = payload => callMutation(payload)
return (
<form onSubmit={handleFormSubmit}>
...
</form>
)
}
Look that onSuccess
callback can provide the request result
and/or variables used in mutation data as additional parameters. The current query data can be replaced by concatenating the old state and the new one or overriding with a single new one.
I think both proposals are great for the dev experience aspect because it's easy to adopt and use (by following common standards of other similar libraries). And because the new two methods are generic, they can be used for other reasons in the future.
It's a bit different as discussed here, if you think makes sense I'll start the new implementation or just implement the know one as @gdorsi suggested.
Thanks for handling this! @mahenrique94
Sorry that I didn't answer in time, but your solution seems to be pretty solid :+1: