apollo-clientreact-apollo

Apollo Client. Why mutation "update" callback is called with unmodified cache second time?


I am trying to use cache.modify to update a field after mutation. This is reactions field of my Message model. The mutation removes reaction from the reactions field. I am using optimistic response.

The strange thing is that update is called with unmodified cache when it's triggered for the second time upon getting backend response.

Here is a small code sample (removeReactionMutation is the mutation function returned by useMutation hook):

   const { data } = await removeReactionMutation({
                    variables,
                    optimisticResponse,
                    update: ((cache, options) => {
                            const { data } = options;
                            console.log('update called', options);
                            console.log(
                                'optimistic cache',
                                JSON.stringify(
                                    cache.extract(true)[GUID2ACID(variables.messageId)].reactions
                                )
                            );
                            console.log(
                                'real cache',
                                JSON.stringify(
                                    cache.extract()[GUID2ACID(variables.messageId)].reactions
                                )
                            );

                            cache.modify({
                                id: GUID2ACID(variables.messageId),
                                fields: {
                                    reactions(existing) {
                                        console.log('removing reaction', {
                                            existing,
                                            userId: identity.guid,
                                            emojiId: variables.emojiId,
                                        });
                                        return existing.filter(
                                            (reaction: IAPIMessageReaction) =>
                                                reaction.userId !== identity.guid ||
                                                reaction.emojiId !== variables.emojiId
                                        );
                                    },
                                },
                                optimistic: true, // this flag seems to have no effect 
                            });

                            console.log(
                                'optimistic cache',
                                JSON.stringify(
                                    cache.extract(true)[GUID2ACID(variables.messageId)].reactions
                                )
                            );
                            console.log(
                                'real cache',
                                JSON.stringify(
                                    cache.extract()[GUID2ACID(variables.messageId)].reactions
                                )
                            );
                        }),
                });

Here is the console.log output:

useEmojis.ts:163 update called {data: {…}} <----- called with optimistic response
useEmojis.ts:164 optimistic cache [... 5 records]
useEmojis.ts:170 real cache [...5 records]
useEmojis.ts:195 removing reaction {existing: Array(5), userId: '...', emojiId: '...'}
useEmojis.ts:210 optimistic cache [...4 records]
useEmojis.ts:216 real cache [...4 records]
useEmojis.ts:163 update called {data: {…}} <----- called with backend response
useEmojis.ts:164 optimistic cache [... 5 records] <----- WHY!?
useEmojis.ts:170 real cache [... 5 records] <----- WHY!?
useEmojis.ts:195 removing reaction {existing: Array(5), userId: '...', emojiId: '...'}
useEmojis.ts:210 optimistic cache [... 4 records]
useEmojis.ts:216 real cache [... 4 records]

So during the first update call the item was removed from cache by cache.modify (both optimistic and main layers). UI is correctly updated and reaction is removed. So far so good!

However, the item still exists in cache and the existing value when update callback is called second time after getting server response!

This is super confusing and it causes problems.

In case update is quickly called multiple times and then it gets called multiple times again as server responses are coming in one by one, the outdated cache and existing value cause the removed items to re-appear for a moment in the UI before they finally get removed.

This is because you have two pending cache.modify calls. Both calls remove its own item but since the first call gets existing value with the other item wrongly present in the array, this very item will re-appear for a moment in the UI after the first cache.modify call. The first call won't remove it (which is expected) and the other item shows up until second call finally removes it. I hope this makes sense.

So in the end you need some external logic to hold all items that should be removed and filter existing so that all items that are about to be removed are excluded from existing value. The need to have an external logic like this doesn't feel right.

Also, I tried calling cache.modify with optimistic flag On/Off but in either cases both cache layers are modified so it's unclear what this flag is about.

My questions is why during the second call of the update callback, cache again holds the deleted item in both layers?

In other words, cache is in unmodified state during second update calls and that's not what is rendered in the UI atm. This is something I can't understand and I'd appreciate any help or thoughts here.


Solution

  • Ok, I figured it out...

    There is no mystery and the cache gets updated because of real-time subscriptions. So it happens outside of the mutation update callback. Between callback runs, cache is modified due to real-time subscription message coming in.

    I'll just keep this answer for anyone who might bump into a similar problem. Most likely you need to look for other places where cache can be updated.