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

defaultOptions fetch policy 'cache-and-network' is not used #6677

Closed
Arquanite opened this issue Jul 23, 2020 · 10 comments
Closed

defaultOptions fetch policy 'cache-and-network' is not used #6677

Arquanite opened this issue Jul 23, 2020 · 10 comments
Milestone

Comments

@Arquanite
Copy link

Intended outcome:
useQuery (and probably others) should use default fetch policy (in this case 'cache-and-network')

Actual outcome:
It does request only once, just like 'cache-first' fetch policy. It seems like it ignores default setting.

How to reproduce the issue:
I edited example from docs to show this bug. Press "Change" button to switch currency. It should make request everytime, but it does only once per currency.
https://codesandbox.io/s/default-options-u7jut

Versions
Apollo-Client: 3.0.2
React: 16.13.0

@maapteh
Copy link

maapteh commented Jul 23, 2020

you are using a graphql endpoint which spins down on no activity, so you first have to open https://48p1r2roz4.sse.codesandbox.io/ in order to see it working, but indeed this is a bug :)

@Titozzz
Copy link

Titozzz commented Jul 24, 2020

I can confirm this issue, I'm investigating with a teammate to see if we can find why 🤷

@adrienharnay
Copy link

adrienharnay commented Jul 24, 2020

@Titozzz and I are pretty sure it's caused by this:

if (fetchPolicy === "cache-and-network" ||
fetchPolicy === "network-only") {
// When someone chooses cache-and-network or network-only as their
// initial FetchPolicy, they almost certainly do not want future cache
// updates to trigger unconditional network requests, which is what
// repeatedly applying the cache-and-network or network-only policies
// would seem to require. Instead, when the cache reports an update
// after the initial network request, subsequent network requests should
// be triggered only if the cache result is incomplete. This behavior
// corresponds exactly to switching to a cache-first FetchPolicy, so we
// modify options.fetchPolicy here for the next fetchQueryObservable
// call, using the same options object that the Reobserver always passes
// to fetchQueryObservable. Note: if these FetchPolicy transitions get
// much more complicated, we might consider using some sort of state
// machine to capture the transition rules.
options.fetchPolicy = "cache-first";
}

When someone chooses cache-and-network or network-only as their initial FetchPolicy, they almost certainly do not want future cache updates to trigger unconditional network requests

It would mean that when using apollo-persist-cache and for, say, a paginated component:

  • first page load ever: I load the 1st page and navigate to the 2nd and 3rd (through the same query, but variables change)
  • some new element gets created (which impacts 1st, 2nd and 3rd pages)
  • I reload the first page, the cache gets updated (for the 1st page)
  • I navigate to the 2nd and 3rd pages (still through the same query, but variables change), my cache is hit, and never gets refreshed (the query is never performed again at all)

That doesn't seem right, or maybe I misunderstood something? 🤔

@adrienharnay
Copy link

@benjamn @hwillson sorry to summon you, do you have an opinion on the matter?

@adrienharnay
Copy link

adrienharnay commented Jul 27, 2020

Some (somewhat organized) thoughts on this issue, after thinking about it this week-end. Please note I do not have full knowledge of the codebase, and while making assumptions I know I might not grasp the whole complexity of the code involved.

  1. I've tried commenting options.fetchPolicy = "cache-first"; and see what happens, since I believe it's not the best behavior and a "dirty fix" for other problems at other places. The next comments will be based on this assumption. Also, I'm testing using apollo-persist-cache.

  2. First problem: at page load, some queries are performed twice, while that was not the case before. Logging here:

    ref.current = { key, value: memoFn() };
    makes me realize the tick gets updated
    if (!queryData.ssrInitiated()) {
    // When new data is received from the `QueryData` object, we want to
    // force a re-render to make sure the new data is displayed. We can't
    // force that re-render if we're already rendering however so to be
    // safe we'll trigger the re-render in a microtask.
    Promise.resolve().then(forceUpdate);
    } else {
    // If we're rendering on the server side we can force an update at
    // any point.
    forceUpdate();
    because onNewData is called.
    That makes the key for useDeepMemo update, and execute is called again.

Yes, the data has changed since the last call (which was the first call, in a loading state, that returned null), but not compared to what I have in the cache (the objects are the same). Also, the condition is based on the loading state, so the tick will get updated anyway.

=> Right now, updating the tick re-renders AND triggers execute(). The condition in onNewData is the only thing that prevents the tick from getting updated -> triggering the call -> tick getting updated -> infinite loop. Maybe re-rendering and re-fetching should be decoupled?

  1. Second problem: the current implementation (with options.fetchPolicy = "cache-first"; commented or not is not optimal with queries that use dynamic variables.

Not commented: as stated in previous comments, the queries won't fire again when the variables change and the cache can be hit (cache-first after first call).

Commented: useDeepMemo only tracks the last call to execute

export function useDeepMemo<TKey, TValue>(
memoFn: () => TValue,
key: TKey
): TValue {
const ref = useRef<{ key: TKey; value: TValue }>();
if (!ref.current || !equal(key, ref.current.key)) {
ref.current = { key, value: memoFn() };
}
return ref.current.value;
}
, so doing variables A then variables B then variables A, useDeepMemo's cache will always be missed.

I would suggest using hashing the key and doing ref.current = { ...ref.current, [key]: value } instead, so that all performed calls can be cached and hit at the same time. That would also get rid of equalify.equal, and I would suggest something like that util https://github.com/Brigad/redux-rest-easy/blob/master/src/internals/utils/hash.js which is super fast. I believe the perfs would be the same, but would need to measure in order to be sure.

I think making these 3 changes would:

  1. avoid queries that don't re-render when changing variables, that can be confusing especially when you don't know the internals of apollo-client. You specified cache-and-network in your app as a default, yet the internals override it
  2. avoid having the same logic for re-rendering and re-fetching, which I believe now only works because of the options.fetchPolicy = "cache-first"; patch
  3. improve useDeepMemo for queries with dynamic variables

Please let me know what you think, and if you agree to at least part of what I think, let me know how I could help 🙂

benjamn added a commit that referenced this issue Jul 27, 2020
PR #6353 seemed like a clever zero-configuration way to improve the
default behavior of the cache-and-network and network-only fetch policies.
Even though the word "network" is right there in the policy strings, it
can be surprising (see #6305) to see network requests happening when you
didn't expect them.

However, the wisdom of #6353 was contingent on this new behavior of
falling back to cache-first not creating problems for anyone, and
unfortunately that hope appears to have been overly optimistic/naive:
#6677 (comment)

To keep everyone happy, I think we need to revert #6353 while providing an
easy way to achieve the same effect, when that's what you want. The new
options.nextFetchPolicy option allows updating options.fetchPolicy after
the intial network request, without calling observableQuery.setOptions.

This could be considered a breaking change, but it's also a regression
from Apollo Client 2.x that needs fixing. We are confident
options.nextFetchPolicy will restore the #6353 functionality where
desired, and we are much more comfortable requiring the explicit use of
options.nextFetchPolicy in the future.
@benjamn
Copy link
Member

benjamn commented Jul 27, 2020

@adrienharnay Thanks for investigating and sharing your reasoning!

We are strongly considering reverting #6353, with an easy way to restore that behavior if you really want it: #6712. Curious to hear if you have any thoughts about that.

As for useDeepMemo, we'd really prefer to avoid using it altogether, and rely instead on other layers of caching. It seems self-defeating to put such a simplistic caching mechanism in front of the InMemoryCache, and useDeepMemo has definitely caused more than its share of problems in the past (as @hwillson can attest).

@benjamn benjamn added this to the Post 3.0 milestone Jul 27, 2020
benjamn added a commit that referenced this issue Jul 27, 2020
PR #6353 seemed like a clever zero-configuration way to improve the default
behavior of the cache-and-network and network-only fetch policies. Even
though the word "network" is right there in the policy strings, it can be
surprising (see #6305) to see network requests happening when you didn't
expect them.

However, the wisdom of #6353 was contingent on this new behavior of falling
back to cache-first not creating problems for anyone, and unfortunately that
hope appears to have been overly optimistic/naive: #6677 (comment)

To keep everyone happy, I think we need to revert #6353 while providing an
easy way to achieve the same effect, when that's what you want. The new
options.nextFetchPolicy option allows updating options.fetchPolicy after the
initial network request, without having to call observableQuery.setOptions.
Specifically, passing { nextFetchPolicy: "cache-first" } for network-only or
cache-and-network queries should restore the behavior of #6353.

This could be considered a breaking change, but it's also a regression from
Apollo Client 2.x that needs fixing. We are confident options.nextFetchPolicy
will enable the #6353 functionality where desired, and we are much more
comfortable requiring the explicit use of options.nextFetchPolicy going forward.
@adrienharnay
Copy link

adrienharnay commented Jul 28, 2020

Thank you for your answer, #6712 seems greats!

As for useDeepMemo, did you already start replacing it with another layer of caching? I'd like to hear more about it :)

@martinjlowm
Copy link

I think it would be a wise decision to document the nextFetchPolicy-trick (at least explain what problem it can solve). I've spent quite some time figuring out why we were seeing infinite queries in multiple places. Currently, I don't see how cache-and-network would work on its own, since the cache is updated on every response with a new reference, causing a re-render through the updated tick and a refetch as Adrien points out. I'd gladly contribute some additional docs :)

@hwillson
Copy link
Member

hwillson commented May 4, 2021

It doesn't sound like there is an outstanding issue here, so closing. Thanks!

@hwillson hwillson closed this as completed May 4, 2021
@adrienharnay
Copy link

Well I guess it is still related to this unresolved issue: #7437

@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

7 participants