Closed hwillson closed 4 years ago
Hi there,
I was following the originating thread and wanted to help set the context for this feature. Is there currently a feature doc around this cache invalidation that gives us an idea of what to expect from apollo in the future?
Why dont we add a prop like
deleted: true
as you can see some of the mapped types
has generated true something like that
deleted: true would be ignored on all queries that are mapped to it
An api that would allow for deletion by typename and id would be very helpful indeed. Refetching queries or updating only work for very simple use-cases.
store.delete('User',1)
I also would appreciate deleting all objects with a given type name, and related, but it would be nice if we could deep clone a cache, safely.
It'd also be nice to be able to clear out the results of a field, maybe even given certain arguments.
Example: I just deleted a Widget
, so all results from Query.widgets
should be cleared. Every other Widget
(and result of Query.widgetById
) that's currently cached is fine to remain in the cache.
Another example: I just disabled a Widget
, so now I want to clear results from Query.widgets(hideDisabled: true)
, but Query.widgets(hideDisabled: false, search: "some name")
could remain cached.
I'm abusing resetStore
now but that really ruins the whole point of using GraphQL... And getting errors like Error: Store reset while query was in flight(not completed in link chain)
which even makes this (very nasty) workaround a pain to use.
I really don't get why I can't just invalidate a part of the store... Seems like a basic feature
Below, a solution for this; hopefully could work for your use cases. It's kinda hacky :disappointed: , but does the job:
In the example below, we needed to delete an Event -- so, to make it work, simply set your mutation's update to something similar to below:
update: store => {
const deletedEvent = {
...store.data.get(`Event:${eventId /* <-- put your ID */}`),
id: '__DELETED__',
};
store.data.set(`Event:${eventId /* <-- put your ID */}`, deletedEvent);
}
additionally, in your queries (which for me was the query for getting Events), set a filter as such:
eventsData: data.events.filter(({ id }) => id !== '__DELETED__')
Thanks, that does work (and is so hacky haha, but at this point anything goes) but the query is run before the update so it doesn't filter out the deleted item. I could use a getter for that but then I'm doing all the graphql stuff through vuex getters which seems like I'm going further away from proper solution :stuck_out_tongue:
I'm gonna see if I can a satisfactory solution by combining apollo-vue and some of these hacks
@Christilut - to delete/invalidate a part of the store, I use the function below, and pass in the name of the query that should be deleted. The function will delete all instances of the query, regardless of parameters. I am using React, but in Vue it should basically be the same except the path to ROOT_QUERY might be slightly different.
import { withApollo } from 'react-apollo';
....
deleteStoreQuery = name => {
try {
// Apollo 1.x
// let rootQuery = this.props.client.store.getState().apollo.data.ROOT_QUERY;
let rootQuery = this.props.client.store.cache.data.data.ROOT_QUERY;
Object.keys(rootQuery)
.filter(query => query.indexOf(name) === 0)
.forEach(query => {
delete rootQuery[query];
});
} catch (error) {
console.error(`deleteStoreQuery: ${error}`);
}
};
In React, 'withApollo' gets exported as part of the component. The Vue equivalent would be required in order for the store to be available.
Suggestion for an api name – client.resetQuery
to invalidate (and delete?) cache. Similar to client.resetStore
.
usage:
client.resetQuery({ query, variables })
Suggestion for an api name –
client.resetQuery
to invalidate (and delete?) cache. Similar toclient.resetStore
.usage:
client.resetQuery({ query, variables })
Help me please sort out, how to use
resetQuery
you suggest in case when three components should reset data to each other. For example: First component:query { blocks(is_visible: true) {name} }
, second:query { blocks(has_data: true) {name} }
, and third one:query { blocks {name} }
, and I add block by mutationblockAdd
.
Should every component know queries for each other to reset? The case also well described here: https://github.com/apollographql/apollo-feature-requests/issues/29
Why dont we add a prop like
deleted: true
as you can see some of the mapped types
has generated true something like that
deleted: true would be ignored on all queries that are mapped to it
This feels like the best approach to me. Apollo should be able to detect that User:1
was returned in an another query and delete that cache. Or in the case of an array that contains User:1
, it should be able to remove that item from the array, or at the very least mark that query as stale and tell it to refetch.
Manually keeping track of all queries that might be affected and all combinations of parameters is very tedious.
However, there's no real standard for that marking something as deleted afaik. Maybe there should be one, but in the meantime I think the following approach would work best.
An api that would allow for deletion by typename and id would be very helpful indeed. Refetching queries or updating only work for very simple use-cases.
store.delete('User',1)
If a standard is established later for marking an item as deleted from the server, then most the code will already be in place - the main difference being we wouldn't need to call store.delete
anymore and it would all be automatic.
@herclogon in your example, since those three separate queries are independently cached, invalidating after you add a block would look something like this in the mutation's update
:
client.resetQuery({query: isVisibleQuery});
client.resetQuery({query: hasDataQuery});
client.resetQuery({query: allBlocksQuery});
Perhaps there could also be a client.resetNamedQuery(queryName /* operationName */)
that invalidates any query with that operationName, regardless of query params. But that's just for convenience.
@AdamYee Going this way each mutation should know queries which must be re-fetched. When you have a lot of components it will increase code viscosity, cause you must add resetQuery
to mutation on "every" component add. In case when each component has his own queries (as graphql recommend, if I understand documentation right) and mutations it looks very strange. Or do you have global (single) mutation list for whole application?
I assume that a mutation should be able to mark part of cache dirty
using cache path
or object type and id
maybe. And each watchQuery
which contains this dirty record should re-fetches a new data automatically.
For now we are using reFetchObservableQueries
method after some mutations, however this leads to over-fetching.
@herclogon You're correct. This is the challenge when managing cache! I'm more just trying to help guide the discussion for the potential API.
Or do you have global (single) mutation list for whole application?
I do realize and have experienced the increase in code viscosity (complexity?). In our codebase, we use a central mechanism that tracks cached queries which we care about and updates them after certain mutations. Shameless plug - my colleague (@haytko) was able to extract this pattern into a link, https://www.npmjs.com/package/apollo-link-watched-mutation. I think you'll find it solves that need to manage cache in one place.
Something like resetQuery
would be useful in a number of situations, but is by no means the end all solution.
Maybe additionally, or instead of client.resetQuery(...)
, how about invalidateQueries
key directly in mutation options, similarly to refetchQueries
.
The params could be the same in both options (passing query names, or specific objects with query and variables), but the difference would be that by invalidating query it is just marked as dirty, meaning that only once the query is reused it would be force fetched. It probably should respect fetchPolicy, e.g. cache-only won't refetch even if invalidated, but otherwise there would be a server hit. If the query is being watched, it is reused immediately, thus refetched with current variables right away. By refetching invalidated query, it would be marked as clean once again and additional query calls would be resolved normally according to fetchPolicy.
I believe we would be able to happily resolve queries with filters from cache and after mutation invalidate all relevant queries at once, e.g. by query name regardless of variables. Of course it would require to know which queries to invalidate after certain mutation, but if we don't have live queries, we always have to know this on client side.
@jpikora As for me it will be useful to have not invalidateQueries
, but invalidateCache
in directly mutation options, to make part of cache dirty using not query
or query name
, but cache path
or cache object id
. For now in my app I use watchQuery
for each visible component, declaration query
of each component in mutation looks not so cool.
If anyone is looking for a workaround in the meantime, good news, there is one. Just pass an update function like this to your mutation.
const update = (proxy) => proxy.data.delete('AnyCacheKey');
And if you don't know the exact key, but want to match against a regex you could do something like this:
const update = (proxy) => Object.keys(proxy.data.data).forEach((key) => {
if (key.match(/someregex/) {
proxy.data.delete(key);
}
});
@joshjg It works according to the apollo devTools/cache panel~!
Is there any downside effect when deleting from proxy.data
directly in such a way?
@joshjg Thanks so much for this, it's far simpler than the workaround that I had implemented! One thing I noticed is that if the mutation you're using this on changes data that's being displayed on the current page without a re-route after the mutation completes then the Query does not seem to get updated. You need to manually call refetch to get it to grab the up to date information from the server. This is at least the case in the current version of react-apollo that I'm using.
If anyone is interested I wrote up an implementation example of the above idea for dealing with paginated queries here https://medium.com/@martinseanhunt/how-to-invalidate-cached-data-in-apollo-and-handle-updating-paginated-queries-379e4b9e4698
@joshjg I'm using proxy.data.delete('Person:ecb8fe44-c7ba-11e8-97ac-b7982643657d')
and can confirm it works by comparing proxy.data.data
before and after. However, all queries, whether they reference the deleted item or not, return undefined
immediately after the delete is run and remain like that until the query is run again. Is there a way to fix this?
@Tam I had the same issue. I figured it was because the delete
method actually removes the entry related to the provided key from the cache but does not removes the references of this entry. Here is what I finally come up with :
import isArray from 'lodash/isArray'
import isPlainObject from 'lodash/isPlainObject'
import { InMemoryCache } from 'apollo-cache-inmemory'
/**
* Recursively delete all properties matching with the given predicate function in the given value
* @param {Object} value
* @param {Function} predicate
* @return the number of deleted properties or indexes
*/
function deepDeleteAll(value, predicate) {
let count = 0
if (isArray(value)) {
value.forEach((item, index) => {
if (predicate(item)) {
value.splice(index, 1)
count++
} else {
count += deepDeleteAll(item, predicate)
}
})
} else if (isPlainObject(value)) {
Object.keys(value).forEach(key => {
if (predicate(value[key])) {
delete value[key]
count++
} else {
count += deepDeleteAll(value[key], predicate)
}
})
}
return count
}
/**
* Improve InMemoryCache prototype with a function deleting an entry and all its
* references in cache.
*/
InMemoryCache.prototype.delete = function(entry) {
// get entry id
const id = this.config.dataIdFromObject(entry)
// delete all entry references from cache
deepDeleteAll(this.data.data, ref => ref && (ref.type === 'id' && ref.id === id))
// delete entry from cache (and trigger UI refresh)
this.data.delete(id)
}
In this manner, delete
can be used within the update
function :
const update = cache => cache.delete(entry)
In many libraries, it's not uncommon for destructive actions on collections to return the destroyed items. What I'd love to see is an optional extension to queries & mutations that's analogous, where your mutation resolver can return an object field that indicates deleted status. Something like:
mutation DeleteEvent($id: ID) {
deleteEvent(eventId: $id) {
id
__typename
__deleted # hypothetical optional built-in boolean field: true indicates Event with this id no longer exists
}
}
Then any cache can watch query and mutation results just like normal and when it encounters an object with __deleted = true
it knows to remove it from the cache. This would be a very natural, granular, flexible, and even automatic.
I could see this fitting in nicely when deleting:
I don't know if the actual mechanism can look like what I showed above without some really big changes to the whole ecosystem, but maybe there's another mechanism to do this.
withApollo
@Christilut - to delete/invalidate a part of the store, I use the function below, and pass in the name of the query that should be deleted. The function will delete all instances of the query, regardless of parameters. I am using React, but in Vue it should basically be the same except the path to ROOT_QUERY might be slightly different.
import { withApollo } from 'react-apollo'; .... deleteStoreQuery = name => { try { // Apollo 1.x // let rootQuery = this.props.client.store.getState().apollo.data.ROOT_QUERY; let rootQuery = this.props.client.store.cache.data.data.ROOT_QUERY; Object.keys(rootQuery) .filter(query => query.indexOf(name) === 0) .forEach(query => { delete rootQuery[query]; }); } catch (error) { console.error(`deleteStoreQuery: ${error}`); } };
In React, 'withApollo' gets exported as part of the component. The Vue equivalent would be required in order for the store to be available.
Doesn't seem to work. I see the query is removed from the list but even calling refetch after has no change to UI.
@developdeez at the end of the component do you have something along these lines?
export default compose(
withRouter,
withApollo,
)(MyComponent);
@chris-guidry Yep.
Parent: export default withAuth(session => session && session.currentuser)( withRouter(withLocation(ProfileSearch)) );
Child (search panel): export default SearchCriteriaPanel;
@developdeez withApollo needs to be included in the export.
@chris-guidry Same result :/
I have also the need for cache invalidation in a other way with caching tags, if there is developer wo want to spend payed time on it and come out with a solution you can pm me and please read this slack message on the community. https://apollographql.slack.com/archives/C10HTKHPC/p1544566073213400
Hi all GraphQL fans, I’m having some technical difficulties with the GraphQL Caching part in combination with server side rendering for a react app. What I have is a GraphQL API server (Drupal) and a SSR react frontend.
The difficulties what I have now are
- Cache invalidation’ I have on my GraphQL server caching tags and I created for that a field that output the following query:
cache { tags }
output:{ cache: { tags: ['setting:contact', 'media:1', 'node:1'] } }
Now I want to have cache invalidation in theInMemoryCache
but that’s not available right now. I see the solution as howdataIdFromObject
works that you can set caching tags for a whole query or just parts of the query where the cache field is available.- Persistent cache When using SSR now the examples that are on the Apollo website are recreating the
InMemoryCache
on every SSR request. I think this is done because other wise the initial state to the client (browser) are too large because the cache gets bigger and bigger. I would like to keep the cache and only send the used cache for that page to the client (browser). In this way you can create a extra caching layer with option 1 (Cache invalidation) so that you have a faster SSR rendering because you don’t need to do a api call to the GraphQL Server.I’m wondering if other people already solved those issues and how? Or someone who can help me to solve those problems I’m willing to pay and I like that the outcome also get shared in the community so that other people can use those solutions.
I found a solution for myself. It's a workaround, but works anyway. I'm using the cache as key-value storage without any additional complexity. It's a solution to invalidate query
in ROOT_QUERY
(not Entity:id
)
The idea is the next:
1) Use withApollo
from 'react-apollo'
, which gives a instance of apollo client
to the props
2) Make writeQuery: client.writeQuery({ query, data })
, where
a) query is a query you want to remove (invalidate), select only__typename
b) data is a data with __typename: null
It should be something like this:
const what = 'post'
const id = 25
const string = `query { ${what}(id: ${id}) { __typename } }`
const query = gql`${string}`
const data = { [what]: { __typename: null } }
client.writeQuery({ query, data })
After some manipulations I've created HOC which invalidates a specific query
after ComponentWIllUnmount
My custom HOC withApollo:
// @flow
import React, { Component } from 'react'
import { withApollo } from 'react-apollo'
import { gql } from 'apollo-boost'
type Props = {
client: any
}
type Params = {
options: Function
}
export default ({ options }: Params) => (WrappedComponent: any) => {
class Enhancer extends Component<Props> {
componentWillUnmount() {
const { variables: { what, id } } = options(this.props)
const string = `query { ${what}(id: ${id}) { __typename } }`
const query = gql`${string}`
const data = { [what]: { __typename: null } }
const { client } = this.props
client.writeQuery({ query, data })
}
render() {
return <WrappedComponent {...this.props}/>
}
}
return withApollo(Enhancer)
}
And React component, enhanced by custom withApollo HOC:
// @flow
import React from 'react'
import { gql } from 'apollo-boost'
import { graphql, compose } from 'react-apollo'
import NewsPost from 'App/components/news/NewsPost'
import withApollo from 'App/enhancers/withApollo'
type Props = {
match: { params: { id: number } },
data: any
}
const GET_POST = gql`
query ($id: Int!) {
post (id: $id) {
id
title
date
content
views
comments
}
}
`
const Post = (props: Props) => {
const { data: { loading, post } } = props
if (loading) return <div>Loading</div>
if (!post) return <div>404</div>
return (
<div>
<NewsPost post={post}/>
</div>
)
}
export default compose(
graphql(GET_POST, {
options: (props) => ({
variables: {
id: parseInt(props.match.params.id, 10)
}
})
}),
withApollo({
options: (props) => ({
variables: {
what: 'post',
id: parseInt(props.match.params.id, 10)
}
})
})
)(Post)
Please, don't care about flow-types, it's temporary.
Ok, let's take a look at the cache:
Before:
ROOT_QUERY
post({"id":37}): Post
▾Post:37
After component WillUnmount:
ROOT_QUERY
post({"id":37}): // <- it's empty and ready to be updated again!
But it's a workaround. I believe someday they make the real cache invalidation.
Are there any updates on this, it would be nice to have a list of actual requirements instead of having a list of hacks.
After many weeks finally I can say that Apollo is incompatible with server-side rendering because of no cache invalidation in Apollo. Any workarounds are bad. My congrats! Please don't use my example above, because you will get a memory leak 😕
@daryn-k You should be creating a new ApolloClient
instance with a new cache for each SSR request (on the server). Otherwise you're potentially reusing data from previous requests made by different users. If you're properly discarding the caches from previous requests, I don't understand why cache invalidation would be a problem for SSR. Do you mean that cache invalidation on the client somehow does not work well with SSR?
@benjamn I've got what do you mean. Yes, I'm creating a new ApolloClient
instance for every new request to the server. But it's not problem. The real problem is next:
mysite.com/post/25
. Server makes a request to the GraphQL server and pass the data to the client in html-code. Right? new InMemoryCache().restore(window.__APOLLO_STATE__)
.mysite.com/post/25
. Client shows me the cached data - with old comments, number of views etc... What if the data is updated on server? I need make a request every time to avoid that.fetchPolicy: 'network-only'
!How to do that on the client-side: don't make a request on first openning the page and make request in second, third openning the page? Right. I need to invalidate the cache in componentWillUnmount()
. But, as we know, there is no cache invalidation in Apollo. I just can destroy entire cache. but it's not a solution. On the other hand, without SSR everything looks good.
Thanks for your attention. I hope for your help.
Ok, guys. I would like to say I like Apollo and its declarative style, which allows you to write 10x less and cleaner code. It's really super cool. But cache invalidation was a problem so far. I found a solution for myself. If you want to invalidate something, use client.clearStore()
. This function destoys whole your cache as you know. To keep some data persistently use old good Redux.
I hope you figured out already that to keep absolutely all your data in Redux store is not a good solution. But you can use Redux still to keep some very small and compact data of your whole app, for example, user data. Obviously, client.clearStore()
doesn't affect your Redux store. If so, cache invalidation works as we wanted.
I would recommend include Redux data with HOC enhancer. Example:
export default compose(
withPost, // <- Apollo's graphql enhancer
...
withApollo, // <- Apollo's client
withUser // // <- This one enhances your component with Redux data
)(Post)
My graphql enhancer:
const withPost = graphql(GET_POST, {
options: ({ match }) => ({
variables: {
id: parseInt(match.params.id, 10)
}
})
})
My Redux enhancer:
// @flow
// Please don't care about flow types any
import React, { Component } from 'react'
import { connect } from 'react-redux'
type Props = any
const withUser = (WrappedComponent) => {
class Enhancer extends Component<Props> {
render() {
return (
<WrappedComponent
{...this.props.ownProps}
user={this.props.user}
/>
)
}
}
return connect(
(state, ownProps) => ({
ownProps,
user: state.user
})
)(Enhancer)
}
export default withUser
After that you'll get { data, client, user } = this.props
in your component. Destroy freely your graphql data
with client.clearStore()
in your componentWillUnmount()
, because you're keeping the persistent data in your small Redux store. I hope you got my general idea.
Has there been any progress on this issue/feature request? Similar to what I've seen in bug reports but I have not seen mentioned in this feature request, I was hoping for a cache-policy that refreshes queries after a configurable amount of time. For example query is run and cached for two minutes then if called again after 2 minutes it invalidates the cache and fetches the query.
Manually calling a clearQuery
method would be a great addition and I want that, but I think a "timeout" cache-policy would be the easiest implementation and cover most use cases.
Is there any need for open source contributors on this issue? To me this is one of the biggest obstacles for effective usage of Apollo client. Would love to help if it will help solve the issue.
- But then I get a twice fetching when I go to this page first time: on server and on client.
@daryn-k Have you tried passing in ssrMode: true
as options when creating the ApolloClient?
https://www.apollographql.com/docs/react/features/server-side-rendering
It's working just fine for me with nextjs, no duplicated queries at all.
Regarding the issue, any news? I wish I could invalidate only my post list every 30 minutes or the comments every 5 minutes or something like that, without any hacks. It's not that hard to implement digging the cache object but I really think an official way is a must-have.
@pmrt, Hi! I'm just using Redux for a persistent data. And now everything works good. What about cache invalidation in Apollo? I clear all data and don't care about that anymore.
@DPflasterer, I was hoping for a cache-policy that refreshes queries after a configurable amount of time. For example query is run and cached for two minutes then if called again after 2 minutes it invalidates the cache and fetches the query.
@pmrt, I wish I could invalidate only my post list every 30 minutes or the comments every 5 minutes or something like that, without any hacks. It's not that hard to implement digging the cache object but I really think an official way is a must-have.
Not sure if I'm misunderstanding your requirements, but this seems straightforward if your post list and comments are individual queries you can use pollingInterval which is in milliseconds(ex: 30 mins = 30 * 60000 ms). More info: https://www.apollographql.com/docs/react/essentials/queries#refetching
const DogPhoto = ({ breed }) => (
<Query
query={GET_DOG_PHOTO}
variables={{ breed }}
skip={!breed}
pollInterval={500}
>
{({ loading, error, data, startPolling, stopPolling }) => {
if (loading) return null;
if (error) return `Error!: ${error}`;
return (
<img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
);
}}
</Query>
);
Now I can see a need to manually invalidate portions of the Apollo cache, but this would only be necessary when you want to update data via Apollo Client based on events external to the Apollo/GraphQL interaction such as when you receive websocket messages in a multi-client scenario.
@pachuka that wouldn't work, as every poll will hit the cache. What the posters above want is for all calls (polled or not) to have a cache-first fetch policy for n minutes, and after those n minutes, switch to network first until called, and back to cache-first once called.
@pachuka Yeah, as @timhwang21 points out: polling will poll every X ms but that's not what we want, that would be useful eg. if you were building a trading website to refresh the stock price every X ms. That's a different thing.
To put it another way — what you would want is just to invalidate the cache for a given query after an expiration time, eg. 10 min. After 10 min the cache is invalidated so if the user keeps browsing the website (SPA) and he goes back to the page again a new query is requested.
It's not just fetching a given query again every X ms, it would only fetch the expired queries if the user goes back to the page again. Also, switching to 'cache-and-network' after X time, fetching the query and then switching back to 'cache-first' policy rather than invalidating the cache for a given query would give the impression of even "faster" UI for the user, but you get the point!
This is indeed a pretty common need for any app and it's annoying to have to resort to hacks to work around it.
My current "solution" is a combination of writeFragment
/readFragment
to add a stale
property to the object in the cache after mutation and then force a fetchPolicy: 'network-only'
if it's stale when querying again (and set it back as not stale on success). But it still feels very hacky and manual though...
I really think Apollo-Server should control how long something is valid.
The team even has made a pretty good api for this with apollo engine
https://github.com/apollographql/apollo-server/tree/master/packages/apollo-cache-control
type Post @cacheControl(maxAge: 240) {
id: Int!
title: String
author: Author
votes: Int @cacheControl(maxAge: 30)
}
const resolvers = {
Query: {
post: (_, { id }, _, { cacheControl }) => {
cacheControl.setCacheHint({ maxAge: 60 });
return find(posts, { id });
}
}
}
Then something from apollo-client
export default graphql(gql`query { ... }`, {
options: {
fetchPolicy: 'cache-first'
fetchPolicyPastMaxAge: 'cache-and-network'
},
})(MyComponent);
Are there any PR's for this work? I would love to help with that but I see so many proposals. Is there any approach that Apollo team will favor?
I think it is safe to introduce another cache strategy as it will not affect any existing app and provide technology differentiator for people who look into cache invalidation.
Additionally, the delete method will be really important to have. In most of the cases mutation that deletes stuff can remove an element from cache in update function and update queries but the fact that there is no delete function makes this hard. This is specifically important when apollo-cache-persist is used as production ready applications will very often need to clear the entire store with cache.
I think those will be separate concerns really so it will be better to split them into separate issues:
client.cache.delete
etc.The workaround I am thinking about using for this right now is (using fetching books as an example): -
Have an entry in the cache for books, author (and any other types I might need cache control over) as booksCacheTimestamps: {id: "whatever", lastFetched: "whenever"} etc.
Have the original single server query use the @client directive so a client resolver "intercepts" the query then: -
const STALE_MILLISECONDS = 300000
import { BOOK_CACHED } from "./queries"
import { BOOK_FROM_SERVER } from "./serverQueries"
const bookTimestamp = booksCacheTimestamps.filter((item) => {item.id === requestedBookId})
If(bookTimestamp doesn't exist or bookTimestamp exists but (bookTimestamp.lastFetched < (Date.now - STALE_MILLISECONDS))){ client.query BOOK_FROM_SERVER } else { client.query BOOK_CACHED }
Then return the results of whichever query is run. BOOK_CACHED would just run what you would expect to be the original query (i.e. without the @client directive).
3. Update the relevant entry in the timestamps array in the cache after the "intercept" query runs.
If anyone can think of a better solution I would love to hear their input.
If anyone is looking for a workaround in the meantime, good news, there is one. Just pass an update function like this to your mutation.
const update = (proxy) => proxy.data.delete('AnyCacheKey');
And if you don't know the exact key, but want to match against a regex you could do something like this:
const update = (proxy) => Object.keys(proxy.data.data).forEach((key) => { if (key.match(/someregex/) { proxy.data.delete(key); } });
Can anyone expound on this explanation? I'm quite new to Apollo but already running into this problem -- I'll take as verbose an explanation as you'll give me :)
I want to share my workaround.
I think the most simple way to handle apollo cache update after delete mutation to deal with the app logic is to change that '_typename:id' entity's certain field's value by getting the query data in mutation's update API.
For example, I have a 'post_page' cache-first query that is called when I am entering the post's page. When I call the post's delete mutation in that page, user is navigated to '/home' page. At the same time, in the mutation's 'update:' api I change that deleted post:id's certain field to 'null'(it can be any thing depends on your logic), this field can 'id' field or dedicated field like deleted_at in post type's graph schema or anything you want.
And then if your user try to enter that deleted post's page again. you can block that navigation and re-navigate to '/home' by checking that specific changed field in router-resolver(in angular term)
I use this method in my app logic. I works pretty well for me :)
The workaround I am thinking about using for this right now is (using fetching books as an example): -
- Have an entry in the cache for books, author (and any other types I might need cache control over) as booksCacheTimestamps: {id: "whatever", lastFetched: "whenever"} etc.
- Have the original single server query use the @client directive so a client resolver "intercepts" the query then: -
const STALE_MILLISECONDS = 300000 import { BOOK_CACHED } from "./queries" import { BOOK_FROM_SERVER } from "./serverQueries" const bookTimestamp = booksCacheTimestamps.filter((item) => {item.id === requestedBookId}) If(bookTimestamp doesn't exist or bookTimestamp exists but (bookTimestamp.lastFetched < (Date.now - STALE_MILLISECONDS))){ client.query BOOK_FROM_SERVER } else { client.query BOOK_CACHED } Then return the results of whichever query is run. BOOK_CACHED would just run what you would expect to be the original query (i.e. without the @client directive).
- Update the relevant entry in the timestamps array in the cache after the "intercept" query runs.
If anyone can think of a better solution I would love to hear their input.
OK, an easier solution to this. Posting it here in the hope others may benefit from it.
After a lot of thinking it through, I decided the simplest way for cache invalidation on server calls was to return to add a lastFetched field to my type: -
type Blah {
id: ID!
lastFetched: String!
}
When I return the data from the server: -
return {
{...BlahData, lastFetched: Date.now()}
}
I am using a String type as Int won't handle timestamp length integers
I then use a wrapper component to control the caching...
return <BlahCacheQuery><BlahReliantComponent></BlahCacheQuery>
import React from "react"
import BLAH_QUERY from "./BLAH_QUERY"
import { Query } from "react-apollo"
// 300000 milliseconds (5 minutes)
const BLAH_QUERY_STALE_MAX = 300000
const BlahCacheQuery = (props) => {
return (
<Query query={ BLAH_QUERY } variables={{id: props.id}} fetchPolicy="cache-first">
{({ loading: cacheLoading, error: cacheError, data: cacheData }) => {
// grab initial cache-first data and render child component
if (cacheLoading || cacheError) {
return (React.Children.map(props.children, child => {
return React.cloneElement(child, {
loading: cacheLoading, error: cacheError, data: cacheData.blah})
}))
} else {
// if data found && data is NOT stale, render child component
if (cacheData && (cacheData.blah.lastFetched >= (Date.now() - BLAH_QUERY_STALE_MAX))) {
return (React.Children.map(props.children, child => {
return React.cloneElement(child, {
loading: cacheLoading, error: cacheError, data: cacheData.blah})
}))
}
// if data not returned or data returned but stale, requery with network-only then render child component
return (<Query query={ BLAH_QUERY } variables={{id: props.id}} fetchPolicy="network-only">
{({ loading: networkLoading, error: networkError, data: networkData }) => {
return (React.Children.map(props.children, child => {
return React.cloneElement(child, {
loading: networkLoading, error: networkError, data: networkData.blah})
}))
}}
</Query>
)
}
}}
</Query>
)
}
export default BlahCacheQuery
Don't know how reliable this workaround is but I currently use:
export const updateCache = (entityName) => {
return (cache) => {
Object.keys(cache.data.data).forEach((key) => {
key.match(`^${entityName}$`) && cache.data.delete(key);
});
client.reFetchObservableQueries();
};
};
// Usage
client.mutate({
mutation: schema.FollowTopic,
variables: { id },
update: updateCache('User'),
});
Migrated from: apollographql/apollo-client#621