Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cache.modify refetches whole query when I try to update cache #7105

Closed
mpaus opened this issue Oct 1, 2020 · 23 comments
Closed

cache.modify refetches whole query when I try to update cache #7105

mpaus opened this issue Oct 1, 2020 · 23 comments

Comments

@mpaus
Copy link

mpaus commented Oct 1, 2020

Hey everyone, I have a little issue when trying to update the cache using the new cache.modify feature.

My GraphQL query looks like this:

query Menus {
    viewer {
      id
      menuConnection {
        edges {
          node {
            id
            menuId
            label
            menuCategoryConnection {
              edges {
                node {
                  id
                  category {
                    id
                    label
                    subcategoryConnection {
                      edges {
                        node {
                          id
                          label
                          subcategoryItemConnection {
                            edges {
                              node {
                                id
                                item {
                                  id
                                  label
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }

When I add a new item to the subcategoryItemConnection and try to update cache after mutation, the cache.modify refetches the entire query as soon as I access a field of a store element, here is the code:

const [moveItem] = useMutation(MOVE_ITEM, {
    update: (cache, { data: { moveItem } }) => {
      cache.modify({
        id: cache.identify(moveItem.subcategory),
        fields: {
          subcategoryItemConnection(){
            /// ...cache update logic
          }
        },
      });
    },
  }); 
    }
  }

So as soon as I access the subcategoryItemConnection field the query gets refetched, any logic that I write inside the modifier function gets ignored. The way I understand it, the modifier function shouldn't refetch the query it should do the opposite, allow me to update the cache without refetching all data from the backend. Can someone please tell me what the issue is or if I am understanding something wrong?

Thanks

@pontusab
Copy link

pontusab commented Oct 2, 2020

From docs:
Like writeQuery and writeFragment, modify triggers a refresh of all active queries that depend on modified fields (unless you override this behavior).

try to add broadcast: false like this:

const [moveItem] = useMutation(MOVE_ITEM, {
    update: (cache, { data: { moveItem } }) => {
      cache.modify({
        id: cache.identify(moveItem.subcategory),
        broadcast: false,
        fields: {
          subcategoryItemConnection(){
            /// ...cache update logic
          }
        },
      });
    },
  }); 
    }
  }

@mpaus
Copy link
Author

mpaus commented Oct 2, 2020

I tried adding it and it still refetches the whole query.

@benjamn
Copy link
Member

benjamn commented Oct 2, 2020

@mpaus Have you see the relayStylePagination helper function (importable from @apollo/client/utilities)?

You might not need to use cache.modify to update these paginated cache fields, since that update can be handled by a field policy generated by calling relayStylePagination():

import { relayStylePagination } from "@apollo/client/utilities"

new InMemoryCache({
  typePolicies: {
    // ...
    Category: {
      fields: {
        subcategoryConnection: relayStylePagination(),
      },
    },
    Subcategory: {
      fields: {
        subcategoryItemConnection: relayStylePagination(),
      },
    },
    // ...
  },
});

You'll need a field policy for each paginated field (like Category.subcategoryConnection or Subcategory.subcategoryItemConnection above), but relayStylePagination should make it easier to abstract away the details of those field policies.

@nahtnam
Copy link

nahtnam commented Oct 5, 2020

Hello, I'm having a slightly different issue. I'm using relayStylePagination as @benjamn mentioned, but I noticed when I do a delete mutation and call cache.evict({ id }) (I verified id is in the format: Type:ID), the item disappears as expected for a second and then the whole paginated field is wiped and refetched. Has anyone else experienced something like this? Tried both the latest stable and 3.3.0 beta 10

EDIT: I fixed it by switching to cache-first instead of cache-and-network

@mpaus
Copy link
Author

mpaus commented Oct 6, 2020

@benjamn I tried it, didn't seem to do anything for me, so I tried to write my own helper function for cache updating after mutation and I figured out that the case I'm trying to achieve doesn't seem to be doable (I may be wrong though). However I did find a solution, in the mutation I return the parent ID and refetch the fields i wanna update.

Here is the code snippet:

const MOVE_ITEM = gql`
  mutation MoveItem($input: MoveItemMutationInput!) {
    moveItem(input: $input) {
      subcategory {
        id
        subcategoryItemConnection {
          edges {
            node {
              id
              item {
                id
                label
              }
            }
          }
        }
      }
    }
  }
`;

I guess because I am updating a single existing parent entity and refetching the changed fields this seems to work automatically. However I still think a more optimal solution would just be to fetch the added subcategoryItem and append it to the cached Connection array instead of refetching the whole array. This is a good workaround because it refetches automatically only the fields I specify.

@pontusab
Copy link

pontusab commented Oct 13, 2020

Okay, I have the same issue now. If I'm adding a new comment in the edge array the whole query is refetch, but if I'm uncommenting the newly added comment everything works.
I want to add a Comment to a Post:

cache.modify({
          id: `Post:${postId}`,
          broadcast: false,
          optimistic: true,
          fields: {
            commentsConnection(existingCommentRefs = {}) {
              const newCommentRef = cache.writeFragment({
                broadcast: false,
                fragmentName: 'Comment',
                data: {
                  user,
                  ...addComment,
                },
                fragment: CommentFragmentDoc,
              })

              return {
                ...existingCommentRefs,
                edges: [ // If i remove this, everything works, totalCount is updated without refetch of the whole query
                  {
                    node: newCommentRef
                  },
                  ...existingCommentRefs.edges,
                ],
                totalCount: existingCommentRefs.totalCount + 1,
              }
            },
          },
        })

@vendramini
Copy link

@pontusab were you able to find a solution?

I'm building a chat with subscription and I would like to update my UI before server's response. My code is almost the same as yours.

@pontusab
Copy link

@vendramini No, still the same issue.

@vendramini
Copy link

vendramini commented Oct 20, 2020

@benjamn I'm sorry to ping you, but I've made some tests here using the official apollo issue reproduction together with my code, so I can compare the behavior. I can't figure out what's going on. And I've found some others posts here with the same topic, also in stackoverflow and spectrum chat. I don't know if we are not understanding and missing something how it works.

Using the official repo, I'm able to use optimisticResponse and it works as expected:

addPerson(
    {
        variables: { name },
        optimisticResponse: {
            addPerson: {
                id: 'me',
                name: 'also me',
                is_optimistic: true,
            }
        }
    }
);

The data returned from useQuery is always defined, and it adds the response from optimisticResponse as expected, then add the real new person typed on input text.

I'm doing the same logic with my chat app, but getting the data from the server:

const {
    data: dataMessages,
    loading: loadingMessages,
    error,
} = useQuery(
    messagesQuery,
    {
        variables: { targetUsername: 'test_username' },
    }
);

const [
    sendMessage,
    {
        loading: loadingSendMessage
    }
] = useMutation(
    sendMessageMutation,
    {
        update(cache, {data}) {
            const message = data.sendMessage;

            cache.modify(
                {
                    fields: {
                        messages(prev = []) {
                            return [...prev, message];
                        }
                    }
                }
            );
        }
    }
);

And the click button:

sendMessage(
    {
        variables: {
            username: 'test_username',
            input: {
                text: 'lorem ipsum dolor sit'
            }
        },
        optimisticResponse: {
            sendMessage: {
                is_optimistic: true,
                created_at: '2020-10-20T01:29:00.687Z',
                id: "123321",
                media: null,
                text: 'lorem ipsum dolor sit',
                __typename: "Message",
                to: {
                    id: "cka8ppaws00cp0966e40ckhlr",
                    __typename: "User",
                    avatar: {
                        id: "ckec1lcyw00z8076623vgmk8b",
                        filename: 'cka8ppaws00cp0966e40ckhlr159848650442320200805_211353.jpg',
                        __typename: "File"
                    }
                },
                from: {
                    id: "ckdhtrk3i000n0866hhckskvg",
                    __typename: "User",
                    avatar: {
                        id: "ckdvsz6eh00tf0866b724h5xo",
                        filename: 'ckdhtrk3i000n0866hhckskvg1597504574221images.jpeg',
                        __typename: "File"
                    }
                }
            }
        }
    }
);

When I'm adding the optimisticResponse into the cache, the dataMessages turns into undefined and restore the value after another server's request, which is wrong. Isn't the expected behavior.

If I add the new message after the server's response, it works, the dataMessages doesn't turns into undefined.

@vendramini
Copy link

vendramini commented Oct 21, 2020

I think the problem isn't related with optimisticResponse, but modify. How can we avoid it to refetch the query? broadcast: false doesn't works and I can't find anything related in docs: https://www.apollographql.com/docs/react/caching/cache-interaction/#cachemodify

It says: "Like writeQuery and writeFragment, modify triggers a refresh of all active queries that depend on modified fields (unless you override this behavior)." but HOW to override this behavior if broadcast: false doesn't works?

I've tried adding nextFetchPolicy: 'cache-only' but it is turning the data into undefined - which doesn't makes sense to me, because the data does exists, I just have added a new one.

@pontusab
Copy link

@vendramini I just solved this by returning the right structure from the backed, I guess the reason for the query to refresh is that the optimistic response is not the same as the actual one from the mutation and therefore it reloads the whole query.

@vendramini
Copy link

I tried with the same structure without success. I gave up and now I'm controlling what I need with a parallel useState... That is sad.

Thank you :)

@ilyagru
Copy link

ilyagru commented Nov 12, 2020

I can confirm, experiencing exactly the same issue. If I update some specific [nested] fields manually on mutation, then it won't make sense to refetch the whole query. If I wanted to refetch the whole query I would call refetch() instead. broadcast: false is not affecting anything either.

@pontusab
Copy link

@ilyagru, make sure that the mutation response data is exactly what you expect. I had the same issue, but looking closer the response lacked some fields.

@ilyagru
Copy link

ilyagru commented Nov 13, 2020

Thanks @pontusab! It seems my issue has been resolved with nextFetchPolicy: 'cache-first'.

@askurat
Copy link

askurat commented Nov 13, 2020

I am having a similar issue except this only happens on root queries. For example, this triggers a refetch (favoriteReportsByUser is a root query):

 addToFavoritesMutation({
      variables: { id, userId, reportId },
      optimisticResponse: {
        __typename: 'Mutation',
        createUserFavoriteReport: {
          __typename: 'UserFavoriteReport',
          id,
          userId,
          reportId,
          report: {
            __typename: 'Report',
            name: reportName,
          },
        },
      },
      update: (cache, { data }) => {
        const favoriteId = data?.createUserFavoriteReport?.id;
        const refId = cache.identify({ __typename: 'UserFavoriteReport', id: favoriteId });

        cache.modify({
          fields: {
            favoriteReportsByUser: (existingRefs = {}, { readField }) => {
              if (existingRefs.items.some((ref: any) => readField('id', ref) === favoriteId))
                return existingRefs;

              return {
                ...existingRefs,
                items: [...existingRefs.items, { __ref: refId }],
              };
            },
          },
        });
      },
    });

This does not (userFavorites is a field inside of the Report type):

addToFavoritesMutation({
      variables: { id, userId, reportId },
      optimisticResponse: {
        __typename: 'Mutation',
        createUserFavoriteReport: {
          __typename: 'UserFavoriteReport',
          id,
          userId,
          reportId,
          report: {
            __typename: 'Report',
            name: reportName,
          },
        },
      },
      update: (cache, { data }) => {
        const favoriteId = data?.createUserFavoriteReport?.id;
        const refId = cache.identify({ __typename: 'UserFavoriteReport', id: favoriteId });

        cache.modify({
          id: cache.identify({ __typename: 'Report', id: reportId }),
          fields: {
            userFavorites: (existingRefs = {}, { readField }) => {
              if (existingRefs.items.some((ref: any) => readField('id', ref) === favoriteId))
                return existingRefs;

              return {
                ...existingRefs,
                items: [...existingRefs.items, { __ref: refId }],
              };
            },
          },
        });
      },
    });

I have added both:

  fetchPolicy: 'cache-and-network',
  nextFetchPolicy: 'cache-first',

to my favorite reports query hook but this doesn't seem to have any effect when updating a root query. I'm also not sure if this is expected behavior.

@milosdavidovic
Copy link

I had the same issue and it turned out that mismatch in data structure caused another network request to be fired after the cache update. Changing fetchPolicy on the initial query to "cache-only" made this visible in the console as I got some warnings after I executed the manual cache update.

I used some aliasing on underling connection (childrenLimited) and it looks like cache.modify can't handle that well:

fragment MicroblogBasic on Microblog {
  id
  message
  creationDate
  childrenLimited: children(first: 1) {
    totalCount
    edges {
      node {
        id
        creationDate
      }
    }
  }
}

Thanks @pontusab for pointing in the right direction.

@benjamn
Copy link
Member

benjamn commented Nov 23, 2020

@milosdavidovic cache.modify operates on internal cache data, after query field aliases have been normalized away, so you should use children rather than childrenLimited in the fields section of your cache.modify call. The childrenLimited alias is just a temporary fiction used for that particular query/fragment, not something that sticks around in the cache.

@milosdavidovic
Copy link

Thanks @benjamn. Yes, I've removed the alias, but also had to remove the (first: 1) part as I was getting MissingFieldError for the children field otherwise.

@aquelehugo
Copy link

aquelehugo commented Dec 14, 2020

We had that issue today and the problem was that the result from the mutation didn't have the same fields the query had.

Our query was like this:

query {
  comments {
    edges {
      id
      content
      totalReplies
    }
  }
}

Our mutation was like this:

mutation {
  addComment {
    id
    content
  }
}

So, when we called cache.modify and added a reference to the recently created comment, the fields didn't match and the query was refetched.

cache.modify({
   fields: {
     comments(previous, { toReference }) {
       return {
         ...previous,
         edges: [toReference(newComment), ...previous.edges],
       };
     },
   },
});

So we updated our mutation to return totalReplies, which was missing in comparison to our query fields and the refetching stopped.

mutation {
  addComment {
    id
    content
    totalReplies
  }
}

Our queries and mutations actually have a more complex structure, but I stripped it down to make the example more readable.

@mpaus
Copy link
Author

mpaus commented Dec 24, 2020

I've managed to fix the issue. Since I reported this issue on 1st of October I don't really remember what my mutation was returning back then or how I tried to update the cache exactly. But this is the code that works for me now, the mutation response looks like this:

mutation AddItemToTree($input: AddItemToTreeMutationInput!) {
    addItemToTree(input: $input) {
      subcategory {
        id
      }
      subcategoryItem {
        id
        item {
          id
          label
        }
      }
    }
  }

The cache update looks like this:

const [addItemToTree] = useMutation(ADD_ITEM_TO_TREE, {
    update: (cache, res) => {
      cache.modify({
        id: cache.identify(res.data.addItemToTree.subcategory),
        fields: {
          subcategoryItemConnection: existingSubcategoryItems => {
            return {
              ...existingSubcategoryItems,
              edges: [
                ...existingSubcategoryItems.edges,
                { node: res.data.addItemToTree.subcategoryItem },
              ],
            };
          },
        },
      });
    },
  });

My conclusion is either the subcategory object I used in cache.identify was not returned correctly or I wasn't returning correctly structured data back to the subcategoryItemConnection field.

@mpaus mpaus closed this as completed Dec 24, 2020
@ghost
Copy link

ghost commented Aug 23, 2021

This was happening to me too, after reading this issue i found indeed the problem was when you run a cache.modify with a fragment structure that is not identical to that of the schema. I was omiting some optional fields, and was getting a refetch. Then i added the fields and it worked.

weird behaviour. I would have espected the ApolloClient to understand optional fields...

@kentastudillo
Copy link

We had that issue today and the problem was that the result from the mutation didn't have the same fields the query had.

Our query was like this:

query {
  comments {
    edges {
      id
      content
      totalReplies
    }
  }
}

Our mutation was like this:

mutation {
  addComment {
    id
    content
  }
}

So, when we called cache.modify and added a reference to the recently created comment, the fields didn't match and the query was refetched.

cache.modify({
   fields: {
     comments(previous, { toReference }) {
       return {
         ...previous,
         edges: [toReference(newComment), ...previous.edges],
       };
     },
   },
});

So we updated our mutation to return totalReplies, which was missing in comparison to our query fields and the refetching stopped.

mutation {
  addComment {
    id
    content
    totalReplies
  }
}

Our queries and mutations actually have a more complex structure, but I stripped it down to make the example more readable.

Thanks a lot! Took me hours to figure this out. 👍

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants