react-apollo-hooks icon indicating copy to clipboard operation
react-apollo-hooks copied to clipboard

Getting stale data from cache

Open dharkness opened this issue 5 years ago • 20 comments

I've got queries and mutations working as hooks, and the mutations are even updating the cache correctly. However, the components using the updated queries get a stale result on the next render and see the update in subsequent renders.

I have a simple list-detail set of components with one route showing all items and another route showing the selected item. This sequence of operations will show stale data for the item when it's loaded in a list but not when it's loaded by itself:

  1. load /#/items
  2. click the fifth one to go to /#/item/5
  3. activate a mutation on the item, e.g. a Disable button
  4. the item will correctly show as "disabled" (up-to-date when single)
  5. click the nav link to go back to /#/items
  6. the fifth will still erroneously show as "enabled" (stale when in a list)
  7. click any item
  8. click the nav link to go back to /#/items again
  9. the fifth will now correctly show as "disabled" (up-to-date on second viewing, even in a list)

Something similar happens when using client.resetStore() and client.clearStore(). Forcing an update on a visible component with a query gets the cached results without hitting the server. Navigating away and back so the component with the stale query data gets destroyed and recreated finally loads the data from the server.

I'm following all the examples I've found, but here's the relevant code just in case:

// List.js

export default function Items() {
    const { data, error, loading } = useQuery(...);
    ... check loading and error ...
    return <ul>
        {data.items.map(item => 
            <li><a href={`/#/item/${item.id}`}>{item.name}</a></li>
        )};
    </ul>;
}

// Detail.js

export default function Detail({ params: { match: { id } } }) {
    const mutate = useMutation(...);
    return <button onClick={mutate}>Disable</button>;
}

I'd greatly appreciate any help or pointers on how to track down this issue in case I'm doing something wrong. Everything else is working fine so I think I've set it all up correctly. Any thoughts?

dharkness avatar Jun 13 '19 03:06 dharkness

we're seeing the exact same thing.

Triggering mutation and moving to a new page, at which point we call useQuery to get the new data, which is out of date at that point. By going back and forward again we can see that it gets the correct data.

G1itcher avatar Jun 13 '19 11:06 G1itcher

do you use the skip flag in the options to try to reload the data?

We have had the same issue when passing the same variables in! It seems skip is not part of the useMemo array that listens for changes: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useQuery.ts

A work around is:

const { data, error, loading } = useQuery(YOUR_QUERY, {
        variables: {
            id
        },
        metadata: !shouldLoadData, // the trick
        skip: !shouldLoadData,
        fetchPolicy: "no-cache"
    });

metadata is part of the changes that useMemo listens to.

otrebu avatar Jun 13 '19 12:06 otrebu

I'm not using skip or changing the fetchPolicy. I assumed Apollo would forget any objects that it altered as the result of a mutation. It's smart enough to update its cache, but it needs a mechanism to update its memoized values as well. Or is the memoization part of the hooks instead of the client?

dharkness avatar Jun 13 '19 16:06 dharkness

@dharkness careful that without skip your query will run on every re-render.

The memoization is part of the custom hook useQuery not the apollo client.

otrebu avatar Jun 13 '19 16:06 otrebu

@otrebu Are you sure that's what skip is for? I'm not seeing queries to my server on each rerender. A new query is sent only when the variables change.

It looks like you use it to skip the query when it would be invalid. In their example, they skip loading the "latest dog photo for a breed" query when there's no breed selected.

<Query
    query={GET_DOG_PHOTO}
    variables={{ breed }}
    skip={!breed}
>

dharkness avatar Jun 13 '19 17:06 dharkness

@dharkness not sure about anything :) There is no mention in the this repo docs. I probably should check the Apollo docs. I had issues in the past that my query was re-running when I didn't want it to. So I usually use it like this:

skip: !breed || dog To not run it if I have the dog data, or if I don't have my variable ready yet.

I didn't have issues, till I had your issue of getting stale data.

otrebu avatar Jun 14 '19 11:06 otrebu

Are you executing multiple queries in that component? In my case, I'm running one query which provides the data. Using skip to skip the query would leave me with no data and nothing to render. With skip set to true, the returned data should always be null. That's what happens when I add it to my query.

From the Apollo docs:

skip: boolean If skip is true, the query will be skipped entirely.

This is different from using no-cache or network-only for `fetchPolicy' to bypass the cache.

dharkness avatar Jun 14 '19 18:06 dharkness

This happens to me too. If I use render components it works

Rockson avatar Jun 15 '19 14:06 Rockson

@dharkness In some places I might be running multiple queries, but in most I don't.

I use useState to store my data from the query. So I use the skip flag to skip if I already have data, or if I don't have the variable yet to query it by.

Yes the fetchPolicy is something different. I just copied and pasted it my scenario above.

I might be triggering more re-renders that I need to, and that seems causing my data to be fetched many times without using skip. That is all. Regardless of the proper usage of skip, hope you tried my solution about stale data :) it worked for me.

Cheers

otrebu avatar Jun 17 '19 09:06 otrebu

I have the same issue, only when navigating between pages after a mutation.

My use case is:

  • List of items + link to a "/new-item" page with form
  • Submit form (prepending new item to cache list in update of mutation) and navigate back to the list
  • New item is not in the list

As mentioned above, if i navigate to another page and back, it appears. Or if i move that form inline to the item list, and submit it, it properly adds it to the cache and appears.

Skip doesnt sound like the right thing at all? is this perhaps a new bug, with react-apollo-hooks/react-apollo?

JClackett avatar Jun 19 '19 21:06 JClackett

metadata: !shouldLoadData, // the trick

This was what made it work for me, not skip.

otrebu avatar Jun 20 '19 08:06 otrebu

Using metadata is definitely a hack, like adding a fake query parameter to defeat the cache. You could also use fetchPolicy: 'network-only', but I don't want to be responsible for invalidating the cache when Apollo already does this for you.

AFAICT, there's a bug in the hook's memoization because the base components supposedly work fine.

dharkness avatar Jun 20 '19 20:06 dharkness

setting errorPolicy to "ignore" is another hack that seems to work for me?

JClackett avatar Jun 20 '19 20:06 JClackett

Setting errorPolicy has no effect for me, and I'm no longer seeing the updated data at any point without reloading the page.

dharkness avatar Jun 20 '19 23:06 dharkness

Using metadata is definitely a hack, like adding a fake query parameter to defeat the cache. You could also use fetchPolicy: 'network-only', but I don't want to be responsible for invalidating the cache when Apollo already does this for you.

It is a hack yes, till someone doesn't look further into it. I called it work around when I first suggested it.

In my case the request wasn't made at all, I tried changing the fetchPolicy, but it didn't work for me.

otrebu avatar Jun 21 '19 08:06 otrebu

Same issue here, working totally fine if I replace the useQuery with the Query component from react-apollo. So definitely a react-apollo-hooks issue

affanshahid avatar Jun 29 '19 19:06 affanshahid

I have this issue too. Appears the useQuery does not listen for cache updates. The following will not cause the "const result = useQuery" to re-render after a mutation.

const result = useQuery(FooGql);
const { query } = result; 

const mutFoo = useMutation(FooGql);
const mutator = () => 
  mutFoo({
    update: (story, { data }) => {
       //update logic
       store.writeQuery({ query, data: newData });
     }
  });

The following will update the cache via the hook and thus the hook see's the update.

  const result = useQuery(FooGql);
  const { query, updateQuery } = result; 
  const mutator = () => 
    mutFoo({
      update: (story, { data }) => {
        //update logic
        updateQuery(() => newData);
       }
    });  

Edit: Further investigation found a const array = useMemo not updating when a complex data structure had it's internal's updated without creating a new object. Please dis-regard this post.

bnowlin avatar Jul 08 '19 16:07 bnowlin

I encountered this too and had to use the metadata hack to work around it. It seems like a pretty basic use case to query for something, edit it in a modal, update it, then expect the underlying page to show the updates. The Apollo cache is correct and I can't think of a scenario where anyone would want useQuery to return something different than is in the cache. Perhaps it could run with cache-only any time skip is false and use the default fetchPolicy only when the variables change.

bdeterling avatar Aug 02 '19 14:08 bdeterling

The issue still exists:

"@apollo/react-hooks": "3.1.3"
"apollo-cache-inmemory": "1.6.3",
"apollo-client": "2.6.4",

TheRusskiy avatar Nov 23 '19 15:11 TheRusskiy

Still exists. Moving to Query component instead

joncursi avatar Feb 08 '20 21:02 joncursi