apollo-feature-requests
apollo-feature-requests copied to clipboard
Instead of updating queries' cache after mutation, let ObservableQuery to watch mutations
Problem
Currently when you run a mutation, there is an update function which can help to update cache for existing queries. The issue is that at that moment you don't have neither query reference nor query variables, so it's hard to read query cache and keep code clean (not interdependent).
Proposed solution
Allow ObservableQuery to subscribe to mutations because at the moment of query creation we have variables and query reference.
For example:
const MY_QUERY = gql`
query getProducts($page: Int!) {
products(page: $page) {
items {
sku
title
}
}
}
`
const CREATE_MUTATION = gql`
mutation createProduct($product: product!) {
details {
sku
title
}
}
`
const query = apollo.watchQuery({
query: MY_QUERY,
variables: { /* ... */ },
})
query.on(CREATE_MUTATION, (current, response) => {
return current.items.concat(response.createProduct.details)
})
query.subscribe((items) => {
// do something with items
})
Mutation subscriptions must be dropped when tearDownQuery is called (basically when there is no subscribers anymore)
Why is it better
Because we have reference to query and query.variables. And the best place to decide whether your query cache needs to react to mutation changes is in query creation, not in mutation creation
P.S.: I've already done this by monkey patching ObservableQuery class
I may have a naive understanding, but I like this approach to cache updates a lot, @stalniy. Are you willing to share your monkey patch or publish it as an npm library?
I actually tried to integrate Vue.js reactivity into Apollo Cache, so the code is a bit messy but you can take a look here: https://github.com/stalniy/apollo-cache-vue-sample/blob/master/src/plugins/apollo.js#L98
The usage in store is here - https://github.com/stalniy/apollo-cache-vue-sample/blob/master/src/store.js#L87
and this is how it's used in component - https://github.com/stalniy/apollo-cache-vue-sample/blob/master/src/components/HelloWorld.vue#L99
This what bother me from time to time:) nice stuff! this should be going forward
In attempting to work-around some query update issues in our Angular project, I hacked together a similar monkey patch of Apollo-Angular based directly on @stalniy's ApolloClient Vue example. Doing so required a bit of reworking since ObservableQuery is not exposed in Apollo Angular to maintain RxJS compatibility but other than exposing the registration method on QueryRef instead of ObservableQuery, the structure is almost identical.
I made two other unrelated significant changes:
- I changed the signature of the query update function registered to the existing
MutationUpdaterFnused in theupdatemethod used in amutatecall so basically the invocation of theupdatefunction on a mutation is passed-through to the registered query update functions. While this requires query update functions to manually read from & write to the cache instead of that being handled automatically, by exposingDataProxyas a parameter to the query updater functions, the updater can perform any other queries or updates that may be needed to correctly update the query in question or application state in general without needing to otherwise inject ApolloClient. - I added an optional, experimental override to removing the query update watches in
tearDownQuery, as I found that unsubscribed queries remaining in the cache otherwise became stale so that when the same query was again invoked, outdated data was displayed. This, of course, is a bad idea, especially since there's no guard against the same update method for the same query and variables.
Updated my repository https://github.com/stalniy/apollo-cache-vue-sample Now it's finished (no support for subscriptions yet) example of Vue and apollo integration with easy cache updates (like Vuex).
Added description in README, so it makes some sense to others
This is how I now can define GraphqlClient
export const graphQlClient = new GraphQlClient({
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
}
},
cache: new ApolloCache({
data: {
ui: {
__typename: 'UI',
token: 'test'
}
}
}),
configureCache: {
getPoints(query) {
query.on(POINT_GQL.create, (current, response, { variables }) => {
if (current.items.length < variables.pagination.pageSize) {
current.items.push(response.createPoint.details)
}
current.meta.total++
})
query.on(POINT_GQL.remove, (current, response) => {
const id = response.deletePoint.details.id
current.items = current.items.filter(item => item.id !== id)
current.meta.total--
})
query.on(FETCH_MORE, (current, response) => {
return {
meta: response.meta,
items: current.items.concat(response.items)
}
})
}
},
link: setContextLink
.concat(new HttpLink({
uri: 'http://localhost:4444/api/gql'
})),
})
Any thoughts from Apollo team?
Can't you follow relay approach?
https://relay.dev/docs/en/mutations#range_add
You should return a new edge on output of your mutation, then you can add the new edge to your existing connections of your store
The big problem with this approach that mutation need to know which connections (I.e. queries to update). So itās a direct dependency between queries and mutations what makes updates handling BIG PAIN.
In the proposed solution, query defines dependency on mutation. This allows for active queries to subscribe to possible mutations in the app and react to them. If query is not active we donāt need to update its cache. Because you will refetch data from server.
This approach scales better and works much better.
The big problem with this approach that mutation need to know which connections (I.e. queries to update). So itās a direct dependency between queries and mutations what makes updates handling BIG PAIN.
There still is a cross dependency on your approach, just the other way around. Note: I'm not dismissing your idea, I rather like it.
Yes. Itās. But itās much easier to handle. Because you have query instance and its variables. So you can easily update cache.
In case when you do this on mutation level, you donāt have query variables.
Usually mutation affects a lot of queries and a query usually is affected only by 2 mutations (create, delete). Update mutation is handled automatically by Apollo
Update: by create and delete I mean cases that adds record to collection or it from there. Sometimes it may include update mutation as well. But again in this case you have query variables so you can effectively identify whether itās a āmoving recordā mutation. Because of the sub/pub pattern you donāt need to iterate over all dependent queries and update their cache. So, the code is simpler and can be moved into centralized place.
Update 2: itās also possible to define which queries are affected by mutation and store dependencies separately (implemented in https://github.com/haytko/apollo-link-watched-mutation) but it involves more complicated logic to achieve this
Not much sure how Apollo store works, but in Relay all queries all stored in a normalized way (read more here https://medium.com/@sibelius/relay-modern-the-relay-store-8984cd148798). So you don't focus on updating queries but to update correctly your store data.
Relay uses hooks or fragmentContainers to keep track of which component needs each data, so if you update the store it will refresh all components that asked for that data
Usually you don't want to update all "queries" or connection after a mutation, this could be very application logic specific
Apollo cache is very similar to relay cache:
- Normalized
- You can read/write fragment directly from/to the cache, having fragmentId (key id in the cache)
- You can read/write query directly from/to the cache, having reference to the query and its variables. usually on mutation side you donāt have variables thatās why
@connectiondirective was added. But it doesnāt help much when you have standard pagination or/and filters. - It supports connection queries
I think that the whole concept of updating cache on mutation is WRONG and came from redux/Vuex experience.
Letās consider CQRS and EventSourcing which is a backend analogy of redux-like solutions on UI. In this case you have 2 separate databases: write db (sequence of events) and read db.
The responsibility of write db is just write events in sequence, validate BL and notify read db that there are changes. Read db subscribed to the events and reacts to events in the way which makes sense for this db. So, it responsible for updating its own cache (usually a worker group related to read model).
If we map this to Apollo cache, we will see that mutations side is a write db (so it just need to send mutation and notify read model). Read model is our graphql queries. So, needs to subscribe to events and update is own cache.
This approach works more naturally and simpler because we separate concerns.
Mutation side should not care what somebody will do with the cache itās responsibility of the cache owner (in this case graphql queries)
Hi, just a question, sorry if it sounds naive: will your approach work after the initial server render ? Does it require the query to have been run once client-side or will it also work if it has only been run server side ?
We'll look into expanding the arguments/options passed to the mutation update function. context and variables have already been added (since this FR was created): https://github.com/apollographql/apollo-client/pull/7902
If you have suggestions for additional context options (e.g. mutation DocumentNode), definitely let us know. Thanks!