Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Refetch queries on underfetch #2003

Merged
merged 11 commits into from
Sep 24, 2018
Merged

Refetch queries on underfetch #2003

merged 11 commits into from
Sep 24, 2018

Conversation

steelbrain
Copy link

@steelbrain steelbrain commented May 23, 2018

Situation

When an object is mounted, and a mutation is executed that returns the same ID as the mounted object but has less fields, loading is returned as false but data is an empty object as returned from this line in apollo-client::QueryManager.

An internal error is generated of course with the message template Cannot find field $FIELDS on Object but that's swallowed and never shown to the user in the code referenced above.

A repro for this error can be found at stevehollaar/react-apollo-error-template@mutate-bug

Possible fixes

There are multiple fixes that were evaluated for this, each with it's own downside

  • Automatically "fill in" the missing fields in outgoing mutations based on the types of results currently being watched, so in case we get a result from server that has ID that's already mounted, we will have all the fields we need. This would've caused a lot of over-fetching and issues in large codebases.
  • Do not automatically update the mounted components, like relay. Let users do it the ...fragment way (which fills in the fields automatically) or specify a manual store updater.
  • After a mutation that causes issues is executed, detect the errors and refetch the query

The last one seemed to be the most straight forward and simple solutions with no side effects so that's what I've done here.

This PR fixes apollographql/apollo-client#3267 #1314

Credits

Thanks @stevehollaar for his repro-case and constant support in this quest

Todo

  • Fix breaking tests
  • Add new tests

@apollo-cla
Copy link

@steelbrain: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/

@jonas-app
Copy link

@steelbrain thanks for your investigation 👍
Sounds like a useable workaround for us.
Will try it in our sample project tomorrow.
apollographql/apollo-client#3267 (comment)

@jonas-app
Copy link

@steelbrain We successfully tested the workaround in our projects. 💯

@mlcohen
Copy link

mlcohen commented May 24, 2018

@steelbrain @jonas-arkulpa This is fantastic! We've been running into this issue and have been debating how to resolve the problem. I agree that automatically "filling in" missing fields is the wrong way to go. We've gone down the path of adding fields causing errors and applying fragments. Not great 😕. Your solution to instead refetch data is a good option, however, I wonder if the query manager could be further refactored so that an optional hook/plugin could be provided to the QM. The QM could then call the hook to delegate how best to handle a detected error. I say this since in our particular scenario, the detected field error pops up not because of direct GQL mutation but, rather, from our system running one or more queries whenever our app receives pushed system events (via websocket). Depending on the circumstance, we could either use a default refetch action (like your solution provides) or in turn perform our own custom error handling that best serves our purposes. Thoughts?

For more detail of the problem we have run into, please see apollographql/apollo-client#2920 (comment)

@steelbrain
Copy link
Author

steelbrain commented May 24, 2018

@mlcohen I also have a branch in apollo-client that returns/exposes partial errors to the codebase so they can be handled but I am not sure if we should expose them to the React component.

The fact that Apollo React Query component returns an object with properties such as loading and data makes it work to our advantage that we can just add it to that object instead of changing function invocation signature.

Thoughts?

Edit: We could also add a prop to the Query component with name noRefetchOnPartial so component could handle the behavior themselves with partialErrors

@mlcohen
Copy link

mlcohen commented May 24, 2018

@steelbrain Yeah, I can certainly see your point directly exposing the error to the React component. We were thinking about updating the apollo client's query manager directly instead of indirectly via a React component. Applying the hook directly against the underlying apollo client instance would provide a way for the system to "globally" handle detected field errors. This means that React components that use apollo client would not be exposed to these types of errors. The global default would be for the QM to do an automatic refetch.

Your idea of adding a noRefetchOnPartial prop also sounds intriguing. Perhaps both options are viable. A "global" approach by directly adding a hook the apollo client's QM and a more isolated approach that overrides the global approach when a noRefetchOnPartial prop is supplied. Hmm 🤔

@steelbrain
Copy link
Author

Because including partialError in React requires that we also expose it in QueryManager I think we should do both.

Other than refetching or handling it on a component-level basis, I am curious how do you intend to handle it directly in Apollo Client? Unless, you are not using React for rendering UI.

@mlcohen
Copy link

mlcohen commented May 24, 2018

In our application we perform GQL queries and mutation through react-apollo's graphql HOC and directly via the apollo client instance calling its query and mutate methods. Most of the time we use the graphql HOC for our React components but where we receive pushed system events, we have actions that use the apollo client instance. Based on these two approaches, it seems that if the partial field is exposed in the result, we could handle the error by doing a refetch in our own way. It would be ideal if the graphql HOC and apollo client's query and mutate could perform a data refetch just like what you're doing in the new Query component. Of course that likely means duplicating the logic in three places to do the same thing. The common point among the three is within the query manager's getCurrentQueryResult method. Perhaps a hook within the QM to handle partial errors isn't necessary since a data refetch seems to suffice, but logic that is more centrally focused to handle partial errors could help.

@peisenmann
Copy link

If you're calling data.refetch(), then you're going to be hitting the network and forcibly updating the local data state to the latest state on the server, right? Even if the user has done a mutation on a small portion of a larger data structure, the user doesn't necessarily want the rest of the cache updated.

What I'd consider the expected behavior is for the response of the mutation to simply overlay/merge into the existing cache, and then fire renders for all subscribers from the current cache state, not from the network response.

@steelbrain
Copy link
Author

@peisenmann While merging is possible, it's also possible that other fields may have changed
by mutation and without refetching you are showing outdated data to the user. This is use-case specific tho.

Either way that cannot be done in react-apollo, it'd have to be done in apollo-cache-inmemory or at least apollo-client. This is the least side effective and straight forward way I've evaluated to be (see first post for other methods considered/tried)

@peisenmann
Copy link

@steelbrain A perfectly valid point, for sure. I'd expect in that case, if you wanted to check for those other fields, you would have them in the query part of your mutation or you would use refetchQueries to trigger the other, bigger query to be re-invoked. By excluding those fields from my mutation, I'm signaling that I don't want them updated at this time.

In an application I'm working on, when a user queues up local changes that are not ready to be persisted to the server yet, we use a writeFragment to write the data back into the Apollo cache. This keeps all views up to date with the user's changes, as long as no rogue refetches are called.

As you've noted, I will take my "expected behavior" points up with the other apollo-* libs. My request for this thread is not to introduce automatic refetches (or at least to allow me to specify noRefetchOnError or something of that nature).

@idris
Copy link

idris commented Jun 1, 2018

@peisenmann see the original issue (apollographql/apollo-client#3267). Normally the mutation payload does get merged into the cache. The code in this PR only gets activated when the mutation payload has under-fetched data, and a component is trying to render with that under-fetched data. In that case you cannot simply merge with the cache because the cache doesn't have what you need.

@peisenmann
Copy link

@idris Ah, okay, so the original full query is not even populated in the first place. @steelbrain Apologies for wasting your time. Thanks for the PR!

@dan-weaver
Copy link

dan-weaver commented Jun 4, 2018

Edit: I think I’ve actually run into a different issue not necessarily related to this PR. I apologize and have reproduced it and will create a new issue from it.

@idris I'm not following what your saying but am desperate to understand this behavior better as I keep running into it. @peisenmann I think this occurs in the case that the original full query is there. Someone correct me if I'm wrong, but that is the behavior I am seeing.

So I'm probably overlooking something and am going to look foolish once you explain @idris, but what do you mean here exactly:

The code in this PR only gets activated when the mutation payload has under-fetched data, and a component is trying to render with that under-fetched data. In that case you cannot simply merge with the cache because the cache doesn't have what you need.

Why is that not possible? You should be able to theoretically merge any fields that were in that mutation payload into the cache and not disturb the ones that it didn't fetch no?

@hwillson hwillson self-assigned this Jun 7, 2018
@felixk42
Copy link

felixk42 commented Jun 8, 2018

Would really love to see this PR merged, it's been consistently a serious deal breaking bug for at least 10 months now...

@hwillson
Copy link
Member

hwillson commented Jun 8, 2018

@felixk42 I'm close to getting this merged in, but this change could have implications on a few edge cases. Regardless, you should see more updates here today.

@jonas-app
Copy link

jonas-app commented Jun 8, 2018

@hwillson thanks for your work & your blog post!

As this issue is the biggest blocker for (not only) our projects I think it clearly needs more thoughts putting into it. We are really happy to finally have a usable workaround, but it clearly is just that, a workaround.

@hwillson
Copy link
Member

hwillson commented Jun 8, 2018

Thanks @jonas-arkulpa!

Just a quick heads up for everyone here. I cleaned things up a bit, ran some more tests, and have had a few more discussions about this approach. While this PR does fix the original issue, we're still a bit concerned about the potential performance impact this could have on larger applications. We're currently digging a bit deeper to see if this can be resolved in another way, but we haven't ruled this potential solution out yet. Sorry for the delay all - this issue has been around for a while, and we can't wait to get it fixed, but we just want to make sure the fix doesn't cause more problems. Thanks for your patience!

@idris
Copy link

idris commented Jun 8, 2018

@hwillson we (me and my team, original authors of this PR) would be happy to contribute to those discussions and provide input. Happy to do that here or in Slack. IMO the more in-the-open the discussion, the quicker we can poke holes and land on something that makes sense.

@pleunv
Copy link

pleunv commented Aug 17, 2018

I don't know why this keeps flying under the radar or if it is deemed not important enough, it's just rather frustrating that something that so drastically changes behavior (and even breaks apps) on a minor version bump gets so little attention.
Is there anything that us users can do to help move this forward?

@cagdastulek
Copy link

+1

steelbrain pushed a commit to Truebill/react-apollo that referenced this pull request Aug 30, 2018
src/Query.tsx Outdated
@@ -172,7 +174,7 @@ export default class Query<TData = any, TVariables = OperationVariables> extends
fetchData(): Promise<ApolloQueryResult<any>> | boolean {
if (this.props.skip) return false;
// pull off react options
const { children, ssr, displayName, skip, client, ...opts } = this.props;
const { children, ssr, displayName, skip, client, disablePartialRefetch, ...opts } = this.props;
Copy link

Choose a reason for hiding this comment

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

I wonder, what is the reason to deconstruct this variable in here when you don't use it later anywhere.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Thanks very much for your work on this @steelbrain! We're going to move ahead with getting this merged, but we've flipped things around a bit so the partial refetch functionality is disabled by default. That will de-risk this PR a bit, and help us get this out sooner than later. If anyone in this PR objects, definitely let me know. Otherwise, the current plan is to get this launched tomorrow. Thanks again for working on this!

hwillson added a commit to apollographql/apollo-client that referenced this pull request Sep 24, 2018
To line up with the React Apollo `partialRefetch` changes
in apollographql/react-apollo#2003.
@hwillson hwillson merged commit 7f6911d into apollographql:master Sep 24, 2018
hwillson added a commit to apollographql/apollo-client that referenced this pull request Sep 24, 2018
To line up with the React Apollo `partialRefetch` changes
in apollographql/react-apollo#2003.
@justinanastos
Copy link

🎉

@mcmar
Copy link

mcmar commented Sep 26, 2018

Please let me know when a new version of react-apollo@2.x.x is released. I really need to get my hands on this ASAP.

@hwillson
Copy link
Member

@mcmar It will be released very soon. The plan was to release it today, but we've hit a bit of a snag with a few dependency issues (unrelated to this PR). They should be resolved shortly, and the release will be pushed out right after (current guesstimate - early am tomorrow). Thanks!

@hwillson
Copy link
Member

React Apollo 2.2.0 is now available. Thanks!

@cagdastulek
Copy link

Great!

I upgraded to 2.2.0 and rerun my tests. Unfortunately, the issue I reported above with a test case was not resolved. Let me know if it is not really something this PR could be able to solve. I will submit it as a separate issue, accordingly.

Thanks.

@jonas-app
Copy link

@cagdastulek Have you enabled partial refetch?
https://github.com/apollographql/apollo-client/pull/3945/files

@cagdastulek
Copy link

Yes, it didn't help either.

@steelbrain
Copy link
Author

For the people that were still having issues with this PR merged, I've opened up a followup PR that should take care of the remaining issues: #2493

Copy link

@vreality64 vreality64 left a comment

Choose a reason for hiding this comment

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

@steelbrain @hwillson
I think this code makes #556 situation. (Bug)

Let's assume using Query component with network-only policy. Query response is always will be cached. It means Object.keys(currentResult.data) !== 0, right?

If then, fetchPolicy is ignored. It will always consume cached result. In other words, if something is already cached, fetchPolicy is ignored

@tnrich
Copy link

tnrich commented Feb 11, 2019

@steelbrain and others, I was thinking it might be nice if this edge case could be detected and the user could be alerted of what is going on without them having to figure it out on their own and find this PR.

I think a short message in the console could be really helpful. Something like:

An error occurred while trying to refetch the query XX from the cache. If you want this query to refetch in this instance please use the partialRefetch: true prop, or if you don't please explicitly pass a disablePartialRefetch: true. More info: #2003 (and a link to the docs)

What do people think about that? IMO it's much better than getting the confusing no error, no data situation.

@heyitsaamir
Copy link

A question related to this, when the query is being refetched, the loading flag is flipped to true for the main query, which shows a spinner in our case. Is there a recommended way of handling this? Thanks for all the work that went into this fix!

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

Successfully merging this pull request may close these issues.

Under-fetching fields of new objects in mutations silently breaks mounted Queries