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

Solve issue #7491 by supporting watchQueryOptions.refetchWritePolicy. #7810

Merged
merged 10 commits into from
Mar 25, 2021

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Mar 9, 2021

As I explained in #7491 (comment), I believe the best way to solve #7491 is to allow writing to the cache in a way that invokes any merge functions with undefined existing data, thereby replacing the field value while still respecting the contract of the merge function.

I also believe this overwrite: true behavior should be the default behavior of the ObservableQuery#refetch method, but I worry that switching the behavior back in a minor version (Apollo Client v3.4) would be a breaking change for any application code that intentionally or accidentally relies on refetch results getting merged with existing data, rather than replacing existing data (which is what refetch was originally meant to do, since AC2).

If we're not comfortable with fixing/breaking refetch in this way, then we need to make the new behavior optional somehow. My best idea right now is to add a new ObservableQuery method, refresh, which is just like refetch except that it writes data to the cache with overwrite: true. I don't love this idea, so I've split it out in to its own commit. If we think we can do without this extra backwards-compatibility measure, my preference would be to revert/remove this commit. I'm curious to hear anyone's (but especially @hwillson and @jcreighton's) thoughts on this question.

Update: see #7810 (comment) and dfc2609 for a better approach, involving a new refetchPolicy option. This approach should be completely backwards compatible because it requires explicitly passing refetchPolicy: "overwrite" to achieve the overwriting behavior.

For this PR, I recommend reviewing commit-by-commit, reading the commit messages for details.

Comment on lines -289 to +271
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== "production" && !context.overwrite) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change means you won't see Cache data may be lost... warnings when writing to the cache with overwrite: true, since we can assume overwriting is the behavior you want.

src/core/networkStatus.ts Outdated Show resolved Hide resolved
Copy link
Member

@hwillson hwillson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the refresh approach is the way to go. We don't want to wait until 4.0 to get this functionality in, and even then changing the behavior of refetch could be jarring. I'm okay with refresh becoming part of our public API, especially knowing refetch is just wrapping refresh (so our maintenance burden isn't being increased by much).

Just wondering out loud (I know we talked about this but I can't remember if we thought of this combo). Did we consider a more explicit name like refetchAndOverwrite instead of refresh? That might make things a bit more clear.

On that note, we'll want to get this added into the API docs as well, here in particular. Thanks for working on this @benjamn!

benjamn added 4 commits March 24, 2021 17:04
By default, cache writes always overwrite existing field data, so this may
not seem like a very consequential change.

However, when a field has a merge function defined, the merge function
will be called whenever incoming data is written to the field, receiving
as its first two parameters (0) any existing data (possibly undefined) and
(1) the incoming data.

Since merge functions generally attempt to combine incoming data with
existing data, when a merge function is defined, there is no easy way to
tell the cache to _replace_ existing field data with incoming field data,
instead of combining them.

The overwrite:true option enables the replacement behavior by causing
undefined to be passed as the existing data to any merge functions
involved in the cache write, so the merge function behaves as if this was
the first time any data had been written for the field in question,
effectively replacing any existing data.
This is how I would ideally like to fix #7491, but I'm worried it could be
a breaking change for any application code that relies on refetch results
getting merged with existing data, rather than replacing it (which is what
refetch was originally meant to do, since AC2).

I will follow this commit with one that makes this the overwrite behavior
opt-in (and thus backwards compatible).
The difference is that refresh overwrites existing fields by not passing
existing data to field merge functions, whereas refetch passes existing
data to merge functions, thereby combining it with the refetch result.

If we continue down this path, we can say "the solution to #7491 is to
switch from using refetch to using refresh," but that involves conscious
effort by the developer, and would mean maintaining two very similar
methods of the ObservableQuery class (refetch and refresh).
benjamn added a commit that referenced this pull request Mar 24, 2021
This reverts commit d87333b and
introduces a new option for WatchQueryOptions called refetchPolicy.

Because of the backwards compatibility concerns raised in #7810, the
default setting for refetchPolicy needs to be "merge", though we hope
developers will switch to refechPolicy:"overwrite" using the explicit
option once Apollo Client v3.4 is released, to achieve the behavior I
described in my comment #7491 (comment).

We try to avoid introducing ad hoc new options, especially when we think
one of the behaviors is preferable (overwriting, that is). However, since
not all call sites of observableQuery.refetch are under the developer's
control (for example, see refetchQueries after a mutation), it seemed
important to have a way to _alter_ the behavior of refetch, rather than an
alternative method (refresh).

If one of these behaviors (hopefully "overwrite") becomes a clear favorite
after Apollo Client v3.4 is released, we may either swap the default or
remove the refetchPolicy option altogether in Apollo Client 4.
@benjamn benjamn force-pushed the 7491-ObservableQuery-refresh branch from 4e3b7eb to fab36e3 Compare March 24, 2021 21:46
This reverts commit d87333b and
introduces a new option for WatchQueryOptions called refetchPolicy.

Because of the backwards compatibility concerns raised in #7810, the
default setting for refetchPolicy needs to be "merge", though we hope
developers will switch to refetchPolicy: "overwrite" using the explicit
option once Apollo Client v3.4 is released, to achieve the behavior I
described in my comment #7491 (comment).

We try to avoid introducing ad hoc new options, especially when we think
one of the behaviors is preferable (overwriting, in this case). However,
since not all observableQuery.refetch call sites are under the developer's
control (for example, see refetchQueries after a mutation), it seemed
important to have a way to _alter_ the behavior of refetch, rather than a
new alternative method (refresh).

If one of these behaviors ("merge" or "overwrite") becomes a clear
favorite after Apollo Client v3.4 is released, we may either swap the
default or remove the refetchPolicy option altogether in Apollo Client 4.
@benjamn benjamn force-pushed the 7491-ObservableQuery-refresh branch from fab36e3 to 59f6ff0 Compare March 24, 2021 21:48
@benjamn
Copy link
Member Author

benjamn commented Mar 24, 2021

@hwillson As we discussed earlier, 59f6ff0 adds a refetchPolicy option to WatchQueryOptions, so developers can choose between the merge or overwrite behaviors, and we can keep the default backwards compatible for now. Just need to write some tests, and then this PR should be ready for review again.

@benjamn benjamn requested review from hwillson and brainkim March 24, 2021 23:10
@benjamn benjamn changed the title Two possible ways of solving issue #7491 Solve issue #7491 by supporting watchQueryOptions.refetchPolicy. Mar 24, 2021
Copy link
Member

@hwillson hwillson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great @benjamn, thanks!

I don't want the similarity of "refetchPolicy" and "fetchPolicy" to
suggest a refetchPolicy is somehow a fetchPolicy for refetches.

I think refetchWritePolicy better reflects purpose of this option, which
is to configure how field data is written into the cache after a refetch.
@benjamn benjamn changed the title Solve issue #7491 by supporting watchQueryOptions.refetchPolicy. Solve issue #7491 by supporting watchQueryOptions.refetchWritePolicy. Mar 25, 2021
@benjamn benjamn merged commit 870285b into release-3.4 Mar 25, 2021
@benjamn benjamn deleted the 7491-ObservableQuery-refresh branch March 25, 2021 22:17
@Akryum
Copy link

Akryum commented Apr 1, 2021

@hwillson As we discussed earlier, 59f6ff0 adds a refetchPolicy option to WatchQueryOptions, so developers can choose between the merge or overwrite behaviors, and we can keep the default backwards compatible for now. Just need to write some tests, and then this PR should be ready for review again.

I disagree with this approach, which means users would need to manually fix refetch while the default behavior is broken compared to Apollo Client 2. Would also mean more work for me in vue-apollo...

@benjamn
Copy link
Member Author

benjamn commented Apr 1, 2021

@Akryum We did it this way to avoid a breaking/surprising change in a minor release. We fully intend to make overwrite the default in the next major release. Do you not think it would be a breaking change to swap the default behavior?

@Akryum
Copy link

Akryum commented Apr 1, 2021

Well, since the default behavior is broken, it's technically a bug fix isn't it?

@benjamn
Copy link
Member Author

benjamn commented Apr 2, 2021

@Akryum I mean, I would love to just fix this, but I am hesitant to release a new major version. What does your intuition as a maintainer say about how disruptive it would be to make overwrite the default in v3.4? (Assuming we leave refetchWritePolicy: "merge" as an optional way to preserve the current/broken AC3 behavior…)

@Akryum
Copy link

Akryum commented Apr 2, 2021

I'm currently experiencing this because I'm migrating a big app from Apollo Client 2 to 3, that's why I'm saying it's a regression compared to the previous major. Looks like some other users are confused too (vuejs/apollo#1115, vuejs/apollo#1113 (comment), maybe vuejs/apollo#1026). IMO it should probably categorized as a bug/regression from v2 and bug fixing it without bumping the major version number would be fine.

@Akryum
Copy link

Akryum commented Apr 2, 2021

If we think about the semantics of fetchMore and refetch, I would say that it is expected that refetch is meant to be used in a case were you want to clear the current result from the cache and load from the server again. So up until now this expected behavior was broken/not working.

@Akryum
Copy link

Akryum commented Apr 2, 2021

As a side note, imo this is all made harder by the fact that merge in the type policies doesn't have a clue if the write is coming from refetch or fetchMore.

benjamn added a commit that referenced this pull request Apr 9, 2021
After discussions in PR #7810 and with the client team, I/we believe the
benefit of restoring the Apollo Client 2.x refetch behavior (that is,
refetches overwrite any existing data), for the specific case of custom
`merge` functions, outweighs the risk of breaking applications that
depended on refetched results getting merged with existing results,
especially since those applications can recover the previous AC3 behavior
by passing refetchWritePolicy: "merge" on a per-query basis, or as a
client-wide default using defaultOptions.watchQuery:

  new ApolloClient({
    defaultOptions: {
      watchQuery: {
        refetchWritePolicy: "merge",
      },
    },
  })
@benjamn
Copy link
Member Author

benjamn commented Apr 9, 2021

@Akryum PR to change the default to refetchWritePolicy: "merge": #7966. Thanks for sharing your concerns!

@dylanwulf
Copy link
Contributor

Wouldn't you want this same behavior when remounting a query with fetchPolicy: 'network-only'? Unless I'm misunderstanding something, I would imagine that remounting a query with the network-only policy would act the same way as a refetch -- ignoring/clearing the cache results and loading from the server again

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants