Apollo-feature-requests: Umbrella issue: Cache invalidation & deletion

Created on 27 Jul 2018  Â·  76Comments  Â·  Source: apollographql/apollo-feature-requests

Migrated from: apollographql/apollo-client#621

project-apollo-client

Most helpful comment

@Abhimanyu310 We're working on this now (design phase). It's going to take some time to complete, as we're building a new cache (that will support normalization, denormalization, cache eviction / garbage collection, and persistence). We're planning on having this ready by https://summit.graphql.com.

All 76 comments

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 to client.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 mutation blockAdd.

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:

  • Ranges of objects (e.g. everything named "abc") where you get back a list of deleted ids
  • Live subscriptions where items can be removed in addition to created/updated.
  • A special "deleted since X" query that returns a list of object ids that have been deleted since some checkpoint X, and includes a new checkpoint.

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

  1. 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 the InMemoryCache but that’s not available right now. I see the solution as how dataIdFromObject works that you can set caching tags for a whole query or just parts of the query where the cache field is available.
  2. 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:

    post({"id":37}): Post
        â–¾Post:37

After component WillUnmount:

     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:

  1. I go to mysite.com/post/25. Server makes a request to the GraphQL server and pass the data to the client in html-code. Right?
  2. Client gets this data from html and restores it like that: new InMemoryCache().restore(window.__APOLLO_STATE__).
  3. The data cached!
  4. After that I go to an another page and return to 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.
  5. Well, I can do that with fetchPolicy: 'network-only'!
  6. But then I get a twice fetching when I go to this page first time: on server and on client.

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.

  1. 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:

  • Direct cache deletion client.cache.delete etc.
  • The new strategy that allows specifying TTLS uses cache etc. (whatever will be best for the community)

The workaround I am thinking about using for this right now is (using fetching books as an example): -

  1. 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.

  2. 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).

  1. 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): -

  1. 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.
  2. 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).
  1. 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'),
});

@BananaBobby on what version of cache this works? What cache implementation are you using?

In InMemoryCache cache.data field is private. If that works that will be real game changer however direct operations on data are hack due to the fact that watchers will not be notified.

@wtrocki
I'm using import { InMemoryCache } from 'apollo-cache-inmemory';.
("apollo-cache-inmemory": "^1.5.1")

This is definitely a hack that I found while playing with cache since reseting store seemed much more like a terrible solution due to the number of requests. Probably there are some side effects like you've mentioned and in my case fetching is rather simple and didn't get affected by that yet.

@BananaBobby Made 2 PR's to make this code less hacky:

1) https://github.com/apollographql/apollo-client/pull/4916
2) https://github.com/apollographql/apollo-client/pull/4917

Ad 1.

This will expose private fields so clients can extend cache implementation and experiment with the invalidation strategies proposed above on your local applications before submitting them as PR.

Ad 2.

This one actually provides invalidation for root only elements which is essential for this feature to move on. Once eviction/invalidation will be enabled community can create a separate link that will contain a number of invalidation rules for cache and timers for those - as suggested in many posts above.

Another idea for invalidation will be to also include additional field like __typename but this will not resolve the problem of large cache that is causing issues with apollo-cache-persist.

How far in the future do we see this feature being implemented?

@Abhimanyu310 We're working on this now (design phase). It's going to take some time to complete, as we're building a new cache (that will support normalization, denormalization, cache eviction / garbage collection, and persistence). We're planning on having this ready by https://summit.graphql.com.

@hwillson I know that I might ask too early but do you see apollo-cache-persist eventually being deprecated or it will still have a role in Apollo 3.0?

I'm asking because the community is working on data filtering feature and performance improvements (storing individual values instead of full persist)

@wtrocki apollo-cache-persist is great, and it's awesome to see the recent traction in that project (thanks to you!). Before you stepped in and started helping out with apollo-cache-persist, we had bounced around the idea of what cache persistence would look like built into Apollo Client. We haven't decided on anything concrete yet (since cache invalidation is a higher priority), but supporting cache persistence out of the box could tie in nicely with the new cache approach we've been considering. @benjamn is leading the design on all of this, so he'll have more to add for sure.

@hwillson Thank you so much! Really excited to see progress in that space and willing to help with the implementation of the new spec.

Hi everyone,
in this repository I created some libraries that extend the functionality of relay and apollo (caching & offline).
In relay-store I natively integrated the persistence through cache-persist and query TTL management.
In apollo-cache I used the library cache-persist as ObjectCache of the apollo inmemory cache.
I would like your opinion and if possible collaborate in order to improve their integration.

Thanks,
Lorenzo

The new InMemoryCache.evict() method is a very good start, but I'm concerned it doesn't solve the biggest pain point I've had with Apollo Caching, namely paginated lists where fetchMore isn't an option (ex. where the UI allows you to choose a specific page instead of continuously loading more).

For that to work, I think you'd have to be able to evict _fields_ rather than objects - ex. all instances of Query.widgets with _any_ arguments. (maybe WidgetConnection would be marked with the new keyFields: false option to prevent them from being normalized)

Agree with @dallonf regarding paginated requests.

I'd like refetchQueries() to refetch active queries and also invalidate any cached queries. Alternately, an invalidateQueries() method could follow the same pattern.

For that to work, I think you'd have to be able to evict fields rather than objects

The community have solution:
https://www.npmjs.com/package/@jesstelford/apollo-cache-invalidation

I'm using a heavily customized version of that package myself that does build abstractions like pagination that is very specific for my needs.

@wtrocki Yes, apollo-cache-invalidation has been how I've solved this problem in the past, but it hasn't been updated in over a year and doesn't seem to have kept up with changes to Apollo's internals.

Field-level eviction is coming in Apollo Client 3, thanks to https://github.com/apollographql/apollo-client/pull/5643.

Funny story: I vaguely remembered reading @dallonf's comment when I implemented that feature, which is why I made sure evicting a field by name would evict all instances of the field within the object, regardless of arguments… but I could not find that comment again for the life of me. Now, at long last, we are reunited!

I use a similar workaround like some of the suggestions here and just wanted to share it.
For example, when I delete a record with a mutation, the mutation resolver returns something like;

{
    __typename: Post,
    id: 1234,
    __deleted: true
}

So, Apollo updates the cached records automatically. Not even need to use the "update" function manually on the client.

The only downside is, the record just doesn't get deleted from the cache actually. I simply use a lightly wrapped useQuery hook and filter out records those have __deleted:true property.

Very, very basic approach, but sometimes it can be messy when dealing with nested data structures.

Instead of filtering these records with a "hacky" way like this and just hiding them from the UI, Apollo might delete the records when some sort of a __deleted field returns from the API.
This would result with a "convention" of course. Some of the developers would be irritated, even if the only thing we need to do is returning a recommended result object when a "delete" mutation occurs on the api.

This is a very simple idea, just wanted to share it. Just solves some of my use-cases.

@dallonf were you able to use evict for the use case you described here

I tried many different ways of implementing that but it never felt right. As you said, it's been the biggest issue I also have with apollo cache

I haven't gotten to use Apollo Client 3.0 yet, but it seems like the field-level eviction described would be able to handle it.

Yes I've been trying to wrap my head around and playing with v3 now that is possible.

I wonder tho if you found a workaround for your use case using v2, before migrating everything to v3

For 1.x, I used https://github.com/lucasconstantino/apollo-cache-invalidation. But if I remember right, it's not compatible with 2.x. So I wound up resetting the entire store whenever anything needed to be invalidated. But _this_ would often throw an unhelpful message: https://github.com/apollographql/apollo-client/issues/3766

Soooo yes I'm very much looking forward to native support in 3.x! 😄

I likewise ran into that issue when trying to reset the store in 2.x and resorted to throwing away the entire Apollo Client object and recreating it from scratch after some sufficiently complex mutations.

More flexible and reliable cache eviction methods are definitely welcomed.

Is this still in roadmap? I need to clear all the permutations of a query, when something is changed by some user and i receive a notification with signalR

If anyone on 3.0 wants to try out a beta we have been using for invalidation policies as an extension of the InMemoryCache it should solve some use cases: https://github.com/NerdWalletOSS/apollo-invalidation-policies

@danReynolds Looks really cool. TTL is something that's def missing from Apollo.

In the repo's example, when an Employee gets evicted, their related EmployeeMessage entities also get evicted. Out of curiosity, how does the repo handle any GraphQL queries that were holding refs pointing to those evicted EmployeeMessage entities? Are they dangling or do they get automatically "pruned" by the package?

My main problem has always been that when you delete an entity, all queries that were having it as a result had issues...

@danReynolds Looks really cool. TTL is something that's def missing from Apollo.

In the repo's example, when an Employee gets evicted, their related EmployeeMessage entities also get evicted. Out of curiosity, how does the repo handle any GraphQL queries that were holding refs pointing to those evicted EmployeeMessage entities? Are they dangling or do they get automatically "pruned" by the package?

My main problem has always been that when you delete an entity, all queries that were having it as a result had issues...

The library is just using onWrite and onEvict policies to control when to run cache.evict and cache.modify, so it doesn't clean up those dangling refs automatically. As Ben talked about in this thread: https://github.com/apollographql/apollo-client/issues/6325#issuecomment-656734309, dangling refs should be filtered out of reads now though, so they've been less of a problem for me now. If you did want to make sure they're removed, you could have an onWrite policy like this:

DeleteEmployeeResponse: {
  onWrite: {
    EmployeeMessagesResponse: ({ modify, readField }, { storeFieldName, parent: { variables } }) => {
      modify({
        fields: {
          [storeFieldName]: (employeeMessagesResponse) => {
            return {
              ...employeeMessagesResponse,
                data: employeeMessagesResponse.data.filter(
                  messageRef => readField('employee_id', messageRef) !== variables.employeeId
                ),
              }
            }
          }
        }
      });
    },
  }
},

but in general I've found it okay to leave the dangling refs in cached queries with the new read behaviour.

@danReynolds That's perfect. Thanks, I somehow missed Ben's PRR on automatic filtering of dangling refs. Good to know that you can confirm no issues were found.

We've adapted @Baloche's deepDeleteAll for our apollo-angular project, which has radically simplified many query updates on deletion since it removes all references to the deleted object. Will this approach continue to work in ApolloClient 3?

Was this page helpful?
0 / 5 - 0 ratings