apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.31k stars 2.65k forks source link

useMutation updates cache but does not re-render components querying same object #5963

Open kkitay opened 4 years ago

kkitay commented 4 years ago

Intended outcome:

Actual outcome:

Some screenshots w/ info redacted. Object A is listing and Object B (the object being created) is latestValidPropertyInspection. The initial query:

Screen Shot 2020-02-18 at 5 39 42 PM

The mutation running and returning the new objects w/ non-null latestValidPropertyInspection:

Screen Shot 2020-02-18 at 5 39 57 PM

apollo chrome extension showing the cache updated after the mutation ran:

Screen Shot 2020-02-18 at 5 40 22 PM

query:

  const result = useQuery({
    query: listingById,
    options: { variables: { id: currentListing.id } },
  });

mutation:

const [requestInspection, result] = useMutation(mutation: requestInspectionMutation);

How to reproduce the issue: Query for a single object in one component, with a field being another object but that is returning null. In another component, mutate to create said object, but return the parent (the original single object). You should find that the component did not render.

Versions


  System:
    OS: Linux 4.19 Debian GNU/Linux 8 (jessie) 8 (jessie)
  Binaries:
    Node: 10.11.0 - /usr/local/bin/node
    Yarn: 1.9.4 - /usr/local/bin/yarn
    npm: 6.4.1 - /usr/local/bin/npm
  npmPackages:
    @apollo/react-hooks: ^3.1.3 => 3.1.3
    apollo-cache-inmemory: ^1.6.3 => 1.6.3
    apollo-client: ^2.6.4 => 2.6.4
    apollo-link: ^1.2.13 => 1.2.13
    apollo-link-error: ^1.1.12 => 1.1.12
    apollo-link-retry: ^2.2.15 => 2.2.15
    apollo-link-state: ^0.4.2 => 0.4.2
    apollo-upload-client: ^11.0.0 => 11.0.0
    react-apollo: ^3.1.3 => 3.1.3
bstro commented 4 years ago

I am running into a curiously similar caching issue. I have an object with a reference that is initially null, but that later gets set to a reference to another object that is already in the cache. When this value changes from null to a reference, the cache indeed updates, but the view is not re-rendered. I'm trying to set up a codesandbox, I'll report back if I get anywhere with it.

edit: @kkitay I attempted to reproduce the error, but so far unfortunately everything works. https://github.com/bstro/react-apollo-error-template I strongly suspect that I have not yet recreated the conditions necessary to produce the error. I'm going to keep working on it tomorrow. If you have any ideas, please let me know, or feel free to create a fork off my fork. 🍻

bstro commented 4 years ago

I found the issue, at least on my end: I was changing the value of the field from null to a reference to an object that didn't exist in the cache yet.

In the development experience, I would kinda expect Apollo to throw an error (or at least a warning) when an entity in the cache has a field that was set to a reference to an object that doesn't exist in the cache.

eugle commented 4 years ago

Using 3.0.0-beta.38 has the same problem, has someone solved it

nelsonpecora commented 4 years ago

I'm encountering a similar issue when I try to mutate edges of an object. For example, let's say I have this in my cache:

Item:abc -> { title: 'parent item', children: [{ __ref: 'Item:def' }] }
Item:def -> { title: 'first child' }

If I mutate the parent (to add another child), my cache updates properly:

Item:abc -> { title: 'parent item', children: [{ __ref: 'Item:def' }, { __ref: 'Item:ghi' }] }
Item:def -> { title: 'first child' }
Item:ghi -> { title: 'second child' }

But no react components are re-rendering, even though the ROOT_QUERY / ROOT_MUTATION are both referencing that parent object:

ROOT_QUERY -> { item(id: 'abc'): { __ref: 'Item:abc' } }
ROOT_MUTATION -> { updateItem({ input: { id: 'abc', child: { id: 'ghi' } } }): { __ref: 'Item:abc' } }
ahouchens commented 4 years ago

I've reproduced this bug.

My context: Using "@apollo/react-hooks"

So in my case, I'm using useMutation() to add a new object, even after I add this new object into the underlying cache using the update() callback, a list component doesn't re-render showing the new object in the list, which uses useQuery() pulling from the same list in cache.

My only workaround right now is setting the fetchPolicy to no-cache on the list view, since the cache being mutated (added to) doesn't force the component to re-render...this is my only option?

Any thoughts on other workarounds?

timothyac commented 4 years ago

Similar problem as @ahouchens . Set my query options { fetchPolicy: 'no-cache' } like mentioned and it seems to work. Currently using '@apollo/react-hooks' & 'react-router-dom@5.1.2'

ahouchens commented 4 years ago

In my case I ended up pursuing the workaround of declaring refetchQueries and listing out the same query my list view needed after the mutation in the useMutation hook.

https://www.apollographql.com/docs/react/data/mutations/#usemutation-api

HugoLiconV commented 4 years ago

any update on this? I'm having the same problem, I have a parent component that fetches an array of orders. An order has the following schema.

type Order {
   id
   status
   #...
}

Then, in a child component, I'm updating the status, when the mutation is done, the cache is updated but it doesn't re-render the parent. This is my mutation:

const ACCEPT_ORDER = gql`
  mutation acceptOrder($orderId: Int!) {
    acceptOrder(order: { id: $orderId }) {
      id
      pharmacyOrderId
      acceptedAt
      status
    }
  }
`;
yankovalera commented 4 years ago

I had this issues before but now with Apollo Client (v3) it's working

samuelseaton commented 4 years ago

I had this same issue and I resolved it by using _.cloneDeep() before using writeQuery() to send the data back to the cache

            const nodes = _.cloneDeep(cacheResults.listPosts.nodes);
            nodes.unshift(result.createPost);
            cache.writeQuery<ListPostsQueryResult, ListPostsQueryArgs>({
                query: LIST_POSTS,
                variables: {
                    clientId,
                    followableId,
                    sortOrder: SortOrder.DESCENDING,
                    cursor: cursor,
                    forward: true
                },
                data: { listPosts: { ...cacheResults.listPosts, nodes } }
            });

I believe the reason is because React was being "smart" and not recognizing I was sending a new object back so it never rerendered the useQuery() hook I was using. After this is worked fine

UthpalaHeenatigala commented 3 years ago

The same thing happen with useQuery. I have the same useQuery in two components. When the cache gets updated the re-render of the second component doesn't happen. This is a primary feature. Am I doing something wrong ?

benknight commented 3 years ago

In my case I had simply had to do a hard refresh of my browser while I was in development mode. Pretty simple but just mentioning for other people who end up here after some Googling.

pavanmehta91 commented 3 years ago

I did 2 things and now it's re-rendering 1) use the client singleton exported from the module instead of cache(proxy) from update function. 2) add writeQuery in setTimeout(()=> //query here ,0); Updates are working if I don't use optimisticResponse otherwise as well. For running optimistic updates I'd to add setTimeout and use the client singleton object.

ssjunior commented 3 years ago

Same here. After I update a component using a modal, the cache is updated, but, does not trigger a re-render in the main ui. Using client 3.3.6. Any ideas?

alamothe commented 3 years ago

@samuelseaton makes sense! Works after I made sure I was properly cloning the object, and not just mutating it.

zibet27 commented 3 years ago

Same here. After I update a component using a modal, the cache is updated, but, does not trigger a re-render in the main ui. Using client 3.3.6. Any ideas?

Try to use @apollo/client 3.3.5 version. Helped to me

asmyshlyaev177 commented 3 years ago

Same problem, using update function with readQuery and writeQuery, cache get updated but React component doesn't. Tried cache.writeQuery and client.writeQuery, same outcome.

Also using cloneDeep, so it's new object for sure.

fromi commented 3 years ago

I have a similar problem too. When the cache is modified, useQuery hook causes a rerender UNLESS the modified object in the cache is something that was not referenced the first time useQuery rendered. Here is my concrete example:

type Me {
  matchmakings: [Matchmaking!]!
  ...
}
type Matchmaking {
  joined: DateTime
 ...
}

Now I have a component that do useQuery to fetch "me", and the matchmakings, in a single query.

  1. Case number 1: the component is rendered only after "me" has already been queried and is in the cache: {me && <MyComponent/>} => In that case, when I do cache.modify to change one of the matchmaking.joined date, the component rerenders (no bug).

  2. Case number 2: the component is rendered before "me" is in the cache. => first time the component renders, "me" is null (loading from backend) => later on, when I do cache.modify to change one of the matchmaking.joined date referred to by the "Me" object that was fetched meanwhile, the component is not rerendered

It is obviously a bug: the only difference between producing it or not is making sure useQuery hook will load from the cache from the first time. Otherwise, there is no update when I change an object in the cache that was not referenced from the start.

To fix the bug, "useQuery" should update properly all the cache references which are listened too to trigger a refresh.

asincole commented 3 years ago

In my case I had simply had to do a hard refresh of my browser while I was in development mode. Pretty simple but just mentioning for other people who end up here after some Googling.

This worked for me as well (using the update function)

raymclee commented 3 years ago

In my case I had simply had to do a hard refresh of my browser while I was in development mode. Pretty simple but just mentioning for other people who end up here after some Googling.

This worked for me as well (using the update function)

hard refresh not work for me. what do you mean you was in development mode?

asincole commented 3 years ago

In my case I had simply had to do a hard refresh of my browser while I was in development mode. Pretty simple but just mentioning for other people who end up here after some Googling.

This worked for me as well (using the update function)

hard refresh not work for me.

what do you mean you was in development mode?

Running the app locally (localhost). Try killing the server and restarting it, then try using another browser

raymclee commented 3 years ago

In my case I had simply had to do a hard refresh of my browser while I was in development mode. Pretty simple but just mentioning for other people who end up here after some Googling.

This worked for me as well (using the update function)

hard refresh not work for me. what do you mean you was in development mode?

Running the app locally (localhost). Try killing the server and restarting it, then try using another browser

oh that doesn't work for me. The initial value in the query is null and after the mutation and the update function. the app doesn't re-render

schriker commented 3 years ago

I have faced exact same issue. I have two components 'A' and 'B', both using the same useMeQuery to fetch user data, but 'B' is rendered only when the user logged in. So my flow looks like this:

The issue is when the user tries to log out. My logout use useMutation with modify method deleting user data from the cache. The thing is useMeQuery triggers component 'B' with updated cache, but 'A' does not.

My solution is to set in component 'A' useMeQuery:

nextFetchPolicy: 'cache-only'
hhoangg commented 3 years ago

Same problem, using update function with readQuery and writeQuery, cache get updated but React component doesn't. Tried cache.writeQuery and client.writeQuery, same outcome.

Also using cloneDeep, so it's new object for sure.

it not work

tehpsalmist commented 3 years ago

It would appear that @fromi 's comment is on the right track. I am seeing similar behaviors when adding an item to a list and then subsequently removing that item. Certain cache udpate operations definitely trigger re-renders, but some do not, and they seem to coincide with the state of the query when it was first loaded.

dan-cooke commented 3 years ago

Is the issue really with re-rendering here?

Maybe i'm experiencing a different issue, but my components will re-render okay, they are just doing so with stale data.

Example:

I make the following query


query getPortfolioById ($id: MongoID!){
  getPortfolioById(_id: $id) {
    _id
    ...PortfolioSummary
    positions {
      _id
      ...FullPosition
      symbol {
        _id
        ...SymbolSummary
          quote {
            _id
            latestPrice
            changePercent
          }
      }
    }
  }
}

Then with my UI, I want to send a mutation to addPosition

As per the Apollo docs on adding an item to a list.

I call cache.modify on the update function of the useMutation like so

 cache.modify({
          id: cache.identify(portfolio),
          optimistic: true,
          fields: {
            positions(existingPositions, { toReference }) {
              return [...existingPositions, toReference(addedPosition?._id)];
            },
          },
        });

Now if i check my cache with the Apollo dev tools (or window.__APOLLO_CLIENT__.cache) I can see the following: image

The cache.modify was succesful.

If I place console.log's in my components that are watching the initial getPortfolioById query - I can see they are triggered after the mutation is succesful.

But the new position has not been added to the data, the InMemoryCache is stale.

My workaround

Use fetchPolicy: 'cache-and-network' - this causes the useQueries to hit the network again after cache.modify which is kind of pointless. I could just have used refetchQueries.

Would aboslutely love this to be fixed

dan-cooke commented 3 years ago

Okay so i've spent the better part of a day investigating this issue by digging into the source code.

But in the end it turned out to be user error.

Make sure the value you return from your cache.modify is in the correct format

{ __ref: `${__typename}:${_id}` }

Here was my broken cache.modify

   cache.modify({
          id: cache.identify(portfolio),
          fields: {
            positions(existingPositions, { toReference }) {
              const ref = toReference(addedPosition?._id);
              // Note: the issue here is that ref is in the following structure
             // { __ref: '6b612136123' }
             // I stupidly thought that the _id would be enough to resolve the proper ref
             // But if you console.log the `existingPositions` refs, they look like this
             // { __ref: 'Position:6b123213} 
             // They include the typename
              return [...existingPositions, ref];
            },
          },
        });

I logged out inside apollo-client, and this was not causing ObservableQuery to reobserve its results from the cache.

So instead of using the toReference utility - use cache.identify on the newly added entity

positions(existingPositions) {
              if (addedPosition) {
                return [
                  ...existingPositions,
                  { __ref: cache.identify(addedPosition) },
                ];
              }

              return existingPositions;

If you ask me, the toReference utility is lacking here - as its pretty muich useless. The Reference that it returns is incorrect.

simonasdev commented 2 years ago

I've also noticed what @dan-cooke said. useQuery hook returns stale data, even though cache is updated correctly. Actually the component calling useQuery won't even re-render, if the mutation call is not at the same component - that is, child components calling mutations that modify parent component query cache will not trigger re-render of the parent component. Really weird behaviour. Can't really even think of a workaround without making a network request by forcing a refetch. Using v3.4.16

videni commented 2 years ago

Confirm no one works , as below. my case is the same as @simonasdev

  const [reset, {loading }] = useMutation(ResetToken, {
    fetchPolicy: 'no-cache',
    // update(cache, { data: { resetSubscriptionToken } }) {
      // options1 : not working
    // cache.modify({
    //   id: cache.identify(customer),
    //   fields: {
    //     subscriptionToken() {
    //       return resetSubscriptionToken;
    //     },
    //   },
    // });
          // options2 : not working
    //   cache.updateQuery({
    //     query: GET_CUSTOMER_PROFILE,
    //   }, ({customer}) => ({
    //     customer: {
    //       ...customer,
    //       subscriptionToken: resetSubscriptionToken
    //     }
    //   }));
    // },
      // options3 : not working

    refetchQueries: ["getCustomerProfile"],

    // options3 : not working

    onQueryUpdated(observableQuery) {
      // Define any custom logic for determining whether to refetch
      // if (shouldRefetchQuery(observableQuery)) {
        return observableQuery.refetch();
      // }
    },
  });
nickisyourfan commented 2 years ago

I had something similar happen - I was querying for nested documents, which were paginated. The query returned the new data to client as I saw the incoming data in the network request. I then followed the documentation to implement a merge function to merge the data with the old data - and confirmed that this too happened inside the apollo client devtools.

The mistake I was making was that the return of the "merge" function inside the typePolicies must be exactly the same as the originating response from the server. Even though the data looked all correct inside the apollo client devtools, the react component was looking for an exact match. It was a good opportunity to write a quick article - in case you want to see a better explanation. Nested Pagination and Apollo Type Policies

iamchubko commented 2 years ago

Had this issue, now fixed.

Two things broke re-render for me:

  1. In cache.modify's fields, not everywhere I was returning refs

    column (existingRefs: Reference[]) {
    if (!data) return existingRefs // always return data you get from the method
    // ...
    return [...existingRefs, newRef]
    }
  2. When you write data to the cache via cache.writeFragment() and you have nested fragments, write only top level fragment. The other fragments will be saved to the cache automatically.

    const newRef = cache.writeFragment({
    id: cache.identify(newData),
    fragment: fragment,
    fragmentName: 'FragmentName',
    variables,
    data: newData,
    })
Hampei commented 2 years ago

The thing that broke re-render for me was that the query was not fetching the id of one of the multiple parent-objects.

After fixing that it all worked automagically again, even though:

I mention this since I read a lot of answers mention it only works when returning a single item, or that you have to return everything in the original query or that react-router breaks things. So spend a lot of time on trying all of those things.

alex-e-leon commented 2 years ago

Like some of the other commenters in this thread, after digging through apollo internals, I found out that the cause for this issue for my case, was due to a Missing ID field while writing result error in ObservableQuery's getDiff call, causing the diffs to fail. In my case, the simple fix was adding the missing id field to my query.

Considering that this error can pretty much break an application - I really think Apollo should be surfacing this warning more prominently during development.

KianooshSoleimani commented 2 years ago

Maybe you didnet use nextFetchPolicy so modify not updated the cache 🤕

BGabrielD commented 1 year ago

having the same issue +1

durdenx commented 1 year ago

Is there a workaround? The cache is totally broken....

dan-cooke commented 1 year ago

I ditched Apollo client ages ago - It’s far too heavy on the client when you can use a SSR framework that can handle most caching / loading / network state out of the box on the server.

the idea of having all this state being tracked on the UI seems ridiculous to me now - not to mention the complicated bugs

avinoamsn commented 1 year ago

Please try to keep comments on-topic and informative.

bpurtz commented 1 year ago

I just battled this issue for like a whole day. Turns out, it was my fault.

Good news is I got a lot more familiar with the apollo cache. If you are returning __typename and id with each type, even if they are nested, it gets flattened and put into the cache. I was seeing the cache update as desired, but wasn't seeing updates in my UI. After digging and digging, I figured out I was not updating the appropriate state variable upon updating of the useQuery data.

So go through all your bases. If you see it updating in cache, chances are you're doing something weird like I was and it's not the libraries fault. If you do not see it updating in cache, then make sure you're returning the correct id and __typename.

This was done with pretty complicated nested references as well, not just simple fields. I believe the object I was updating was 3 layers deep, but that really doesn't matter if you're caching things correctly, because Apollo will flatten that down when it puts it into cache, and then use references.

I am on version 3.7, so the most recent at the time of writing

avinoamsn commented 1 year ago

@bpurtz I believe the issue here is that the calling component does see its UI updated, but any components that rely on the same data (but are not direct children of the calling component) do not refresh. Was this your issue, or was your calling component failing to refresh as well?

bpurtz commented 1 year ago

@bpurtz I believe the issue here is that the calling component does see its UI updated, but any components that rely on the same data (but are not direct children of the calling component) do not refresh. Was this your issue, or was your calling component failing to refresh as well?

Yes, that was my issue as well. I was updating a field type that was a nested field of a query on a parent component.

I would highly recommend getting Apollo devtools up and running to ensure you’re updating the cache correctly. If you’re updating the correct object in the cache, all affected queries should update their respective UI. As I said, it worked for me

airtonix commented 1 year ago

Can we try to keep comments on-topic and informative?

Yeah sure...

The problem is that "Apollo Client Best Practice" is wonderfully overcomplicated.

What discussions need to be had and what butts need fires lit under them in order to make the "Apollo Client Best Practice" easy?

HassanHeydariNasab commented 1 year ago

In my case, using apolloClient.cache.updateQuery after mutation done (a resolved promise from execution or onCompleted callback) instead of cache.updateQuery inside of "useMutation update", resolved the problem. In both case I see that cache is updated on Apollo Client dev-tools.

alessbell commented 1 year ago

Hi all 👋 I'm trying to determine what the root issue is here - does anyone have a minimal, runnable reproduction?

Many of the comments are related to Missing ID field while writing result errors which were ultimately fixed in application code; I'd happily dig into a reproduction, but in the absence of one I'll be closing this out in the next week or so. For additional troubleshooting help, feel free to join our Discord server, thanks!

RemyMachado commented 1 year ago

@alessbell The issue occurs when a component A queries Object A, and another unrelated component B make a mutation that returns an updated version of Object A (cache getting correctly updated). I assumed that apollo would re-render all my components A in which I query for Object A, but that's not true. I don't think it's a bug, but rather a weird behavior. What we do want is an answer to "How to re-render components A when the cache has been updated correctly by an unrelated/far component?"


Currently I'm using a React.Context to pass a refetchOnMutationCompleted function through a big part of my React tree, but this is ugly and a pain to maintain.

Theodore-Kelechukwu-Onyejiaku commented 1 year ago

My Solution

I had to make sure that the mutation returned the correct data. I realized I wasn't returning the id field from my mutation.

My Case

It is true that a certain child component tries to update the cache and in turn, the parent component or other component should make the query thereby rerendering it.

Initially, I thought this was my problem until I logged out what was returned by my mutation:

 deleteClient({
            variables: { "id": client.id },
            update(cache, { data }) {
                const { clients }: any = cache.readQuery({
                    query: GET_CLIENTS
                });
               // I logged out data here
                console.log(data)
                cache.writeQuery({
                    query: GET_CLIENTS,

                    data: {
                        clients: clients.filter((client: ClientInterface) => client.id !== data.deleteClient.id)
                    }
                })
            }
  })

I realized I was not even returning the id I needed for data.deleteClient.id.

So, I had to make sure I returned the correct data from my mutation

export const DELETE_CLIENT = gql`
    mutation DeleteClient ($id: String!){
        deleteClient(id: $id){
            id
            name
            email
            phone
            picture
            gender
            street
            country
            age
        }
    }

And viola 🥳 my app is working fine!!!!

Let me know if this helped you.

Virgilio-Flores commented 1 year ago

Will the components re-render when re-fetching queries that were called from server-side on nextjs?

shesupplypi commented 1 year ago

Had the same issue as many others had mentioned, and after seeing the comments saying it was a "user error", I dived deeper to realize, for my case, it was also a user error. I was using cache.writeFragment when I should have used cache.modify. Here is what helped me understand the issue:

chrisregner commented 1 year ago

In my case I had simply had to do a hard refresh of my browser while I was in development mode. Pretty simple but just mentioning for other people who end up here after some Googling.

This worked for me as well (using the update function)

hard refresh not work for me. what do you mean you was in development mode?

Running the app locally (localhost). Try killing the server and restarting it, then try using another browser

This actually worked for me. What did not work is "Empty cache and hard reload" on Chrome. What worked is opening another browser, and restarting my vite server. I'm using Apollo Client 3.8.1.

I wasn't doing update function, my mutation only updates a single entity already in query prior, and I also verified that the cache did update just the query hook is not updating

EDIT: It happens again as I develop, I find myself having to try to clear everything on browser to get it working again. EDIT: Probably there's more to this, this only works sometimes, will spend today again to figure out what's happening

chrisregner commented 1 year ago

Alright, it was confusion on my part (though documentation could have been more explicit.)

A lot of posts I've read, even the documentation like below, hint that adding/deleting a new entity will not update queries of list of the same entities.

image

That was obvious enough to me, obviously we can't tell if what we deleted or added meet the criteria of whatever list of entities that we have. Maybe we're quering all Person with names that start with "A", for example, so Deleting "Bella" or adding "Chris" shouldn't affect the list, but these are all logic that Apollo has no way to figure out.

What I was missing is merely update of single entity falls to same predicament. On surface I thought Apollo should've been updating it if I simply changed "Age" of Person Apollo would "obviously" just update that entity, because that entity is already "known" to belong to that list already.

But now that I had more time to think, I'm wrong, merely updating fields of an Entity, for example if we updated the name, also potentially mean that the query itself is invalidated, and again Apollo has no way to know about it.


Solution

Alternatively you can just write update function but I prefer relying on response for simplicity

BEFORE

  query Team {
    id

    users(first: 50) {
      id
      name
    }
  }

  mutation UpdateName {
    mutation updateName() {
      ...on User { // <-- this is not enough
        id
        name
      }
    }
  }

AFTER:

  query Team {
    id

    users(first: 50) {
      id
      name
    }
  }

  mutation UpdateName {
    mutation updateName() {
      ...on User {
        id

        team {
          id

          users(first: 50) { // <-- this is what we need, notice that parameters are the same
            id
            name
          }
        }
      }
    }
  }

Lastly, will mention that the fact that it's working sometimes as I wanted definitely tripped me up and led me to think that my wrong assumption was the expected behavior and it's just bugged.