6
votes

I am using GraphQL with Apollo-Client in my React(Typescript) application with an in memory cache. The cache is updated on new items being added which works fine with no errors.

When items are removed a string is returned from GraphQL Apollo-Server backend stating the successful delete operation which initiates the update function to be called which reads the cache and then modifies it by filtering out the id of the item. This is performed using the mutation hook from Apollo-Client.

const [deleteBook] = useMutation<{ deleteBook: string }, DeleteBookProps>(DELETE_BOOK_MUTATION, {
    variables: { id },
    onError(error) {
      console.log(error);
    },
    update(proxy) {
      const bookCache = proxy.readQuery<{ getBooks: IBook[] }>({ query: GET_BOOKS_QUERY });
      if (bookCache) {
        proxy.writeQuery<IGetBooks>({
          query: GET_BOOKS_QUERY,
          data: { getBooks: bookCache.getBooks.filter((b) => b._id !== id) },
        });
      }
    },
  });

The function works and the frontend is updated with the correct items in cache, however the following error is displayed in the console:


Cache data may be lost when replacing the getBooks field of a Query object.

To address this problem (which is not a bug in Apollo Client), define a custom merge function for the Query.getBooks field, so InMemoryCache can safely merge these objects:

  existing: [{"__ref":"Book:5f21280332de1d304485ae80"},{"__ref":"Book:5f212a1332de1d304485ae81"},{"__ref":"Book:5f212a6732de1d304485ae82"},{"__ref":"Book:5f212a9232de1d304485ae83"},{"__ref":"Book:5f21364832de1d304485ae84"},{"__ref":"Book:5f214e1932de1d304485ae85"},{"__ref":"Book:5f21595a32de1d304485ae88"},{"__ref":"Book:5f2166601f6a633ae482bae4"}]
  incoming: [{"__ref":"Book:5f212a1332de1d304485ae81"},{"__ref":"Book:5f212a6732de1d304485ae82"},{"__ref":"Book:5f212a9232de1d304485ae83"},{"__ref":"Book:5f21364832de1d304485ae84"},{"__ref":"Book:5f214e1932de1d304485ae85"},{"__ref":"Book:5f21595a32de1d304485ae88"},{"__ref":"Book:5f2166601f6a633ae482bae4"}]

For more information about these options, please refer to the documentation:

  * Ensuring entity objects have IDs: https://go.apollo.dev/c/generating-unique-identifiers
  * Defining custom merge functions: https://go.apollo.dev/c/merging-non-normalized-objects

Is there a better way to update the cache so this error won't be received?

2

2 Answers

6
votes

I too faced the exact same warning, and unfortunately didn't come up with a solution other than the one suggested here: https://go.apollo.dev/c/merging-non-normalized-objects

const client = new ApolloClient({
  ....
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          getBooks: {
            merge(existing, incoming) {
              return incoming;
            },
          },
        },
      },
    }
  }),
});

(I am not sure weather I wrote your fields and types correctly though, so you might change this code a bit)

Basically, the code above let's apollo client how to deal with mergeable data. In this case, I simply replace the old data with a new one.

I wonder though, if there's a better solution

2
votes

I've also faced the same problem. I've come across a GitHub thread that offers two alternative solutions here.

The first is evicting what's in your cache before calling cache.writeQuery:

  cache.evict({
    // Often cache.evict will take an options.id property, but that's not necessary
    // when evicting from the ROOT_QUERY object, as we're doing here.
    fieldName: "notifications",
    // No need to trigger a broadcast here, since writeQuery will take care of that.
    broadcast: false,
  });

In short this flushes your cache so your new data will be the new source of truth. There is no concern about losing your old data.

An alternative suggestion for the apollo-client v3 is posted further below in the same thread:

cache.modify({
  fields: {
    notifications(list, { readField }) {
      return list.filter((n) => readField('id', n) !==id)
    },
  },
})

This way removes a lot of boilerplate so you don't need to use readQuery, evict, and writeQuery. The problem is that if you're running Typescript you'll run into some implementation issues. Under-the-hood the format used is InMemoryCache format instead of the usual GraphQL data. You'll be seeing Reference objects, types that aren't inferred, and other weird things.