-
Notifications
You must be signed in to change notification settings - Fork 786
Conversation
Ironically, now the “♻️” in the repo description means something 😊 |
Wow, @calebmer this sounds perfect for what is required! Like you said, I assume there will be some minor bugs associated with this change, but hopefully they'll be much easier to squash than the main I for one will be pulling this change asap and will start testing immediately. |
Ok, just added the test which tests the actual behavior we want to fix with this PR ( |
@calebmer I think this is a great approach, there's just one thing I have a question about. Since we are keeping an open subscription to each query, we are going to be reading the results out of the store even though we aren't going to be using them. If the queries are very large and there are a lot of them that could create some CPU impact. |
@stubailo this is an unfortunate side effect. We could disable this by exposing some way in |
Personally I think I would solve this by adding a flag in Apollo client watchQuery which makes the observable "hot" and doesn't track subscribing and unsubscribing, then always pass that flag in React-Apollo. It doesn't seem like it would take a lot of work and the benefits could be worth it. |
We could merge and publish this first then release the other thing as an optimization. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great @calebmer, thanks a lot for implementing a fix that is a viable workaround for most people!
I have some comments about the tests, but I think we can ship this as is. I'm fine with keeping an observable subscription around for each component that was previously mounted, but we should make a note for the fix in Apollo Client if performance becomes an issue. In the medium term we should move to hot observables that have to be explicitly stopped and removed.
src/graphql.tsx
Outdated
* An observable query recycler stores some observable queries that are no | ||
* longer in use, but that we may someday use again. | ||
* | ||
* Recycling observable queries avoids a few nasty bugs that may be hit when |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be helpful to link to the issue here, which has a fuller description. I also don't think it's right to describe this as a bug, I would rather call it a missing feature (the ability to update parts of the store that have no active query watching them). Also, calling reducers/updateQueries multiple times may actually be necessary to reach all corners of the cache, so that could be considered a feature rather than a bug. Those nuances are what made this issue so tricky in the first place.
It's also important to note that recycling query observables will still not provide the ability to update parts of the cache for variables more than one step back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also important to note that recycling query observables will still not provide the ability to update parts of the cache for variables more than one step back.
Did not think about that as a use case we should look at supporting. I don’t think the cost of keeping around all observable queries is worth that, however. Hopefully imperative read
and write
methods will allow users to implement this themselves where necessary.
|
||
this.observableQueries.push({ | ||
observableQuery, | ||
subscription: observableQuery.subscribe({}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should modify apollo-client so an active subscription isn't necessary to keep the query in the store.
|
||
wrapper.unmount(); | ||
done(); | ||
}, 10); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are you picking the timeouts? It's 10 here, it was 5 in the other tests. it might be good if there was a constant for these somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arbitrarily picking numbers for no reason 😉
This is a very unimportant number that could go anywhere from 0–1000 and the test would behave the same.
expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1', '2']); | ||
const queryObservable3 = (client as any).queryManager.observableQueries['1'].observableQuery; | ||
const queryObservable4 = (client as any).queryManager.observableQueries['2'].observableQuery; | ||
expect(queryObservable3).toBe(queryObservable1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apollo Client queryIds are strictly incrementing, so looking for observableQueries[1]
twice will always either return the same object or undefined
if that observable query has been removed. So I think testing that queryObservable3
is equal to queryObservable1
doesn't actually show anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If queryObservable3
could be undefined
if it was removed, then this test does show something, right?
What I really wanted to test was the ObservableQuery
instance directly on the components, but that value is really hard to get.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, then maybe compare directly to undefined and write a comment about what we'd really like to check here.
}, 10); | ||
}); | ||
|
||
it('will not recycle parallel GraphQL container `ObservableQuery`s', done => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what this test is trying to do. Is it supposed to show that if you have the same query rendered twice you actually get two different observable query objects? If so, why is there a remount()
in there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. It shows that if you render two components with the same query you get two observable queries, but if you unmount and remount one of the components it will recycle that observable query so that in total you only ever have two observable queries.
@@ -394,4 +394,218 @@ describe('mutations', () => { | |||
renderer.create(<ApolloProvider client={client}><Container id={'123'} /></ApolloProvider>); | |||
}); | |||
|
|||
it('will run `updateQueries` for a previously mounted component', () => new Promise((resolve, reject) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is really long and keeps track of a lot of stuff, so it would be useful to have a short comment here that summarizes what it does.
I've been testing this since @calebmer made the PR and so far, so good, no new bugs have been found and it definitely resolves the issue. |
@calebmer I've added you to |
Released in |
Rather than making the I just got really confused in a testing scenario where I was setting up a new provider and new client for each test case, but queries get reused anyway. |
@tmeasday There is one recycler per every HOC component. So it isn’t technically global, but every HOC component has one and only one recycler, but I do see the problem 😊 A solution would be to, as you suggest, create a |
Could we move the |
No, the point of the recycler is that observable queries will be recycled across component instances. If we remove that capability then it is basically pointless and we are back where we started as far as the I think the |
Makes sense, I opened: #513 |
I was locked down on apollo-client 0.5.21, react-apollo 0.7.3 for a while due to apollographql/apollo-client#1129, however Is there a step I'm missing? |
@booboothefool I noticed you are using React Native. Maybe you need to clear the cache? |
@calebmer I'm pretty sure everything is installed correctly / cache cleared, etc. I even tried the new There might be something wrong with my implementation for this particular part I am trying it on because apollo-client 0.5.21, react-apollo 0.7.3: apollo-client 1.0.0-rc.6, react-apollo 1.0.0-rc.3: The scenario is very similar to what you mentioned here:
If I downgrade back to 0.5.21, react-apollo 0.7.3, UPDATE: So I was able to get UPDATE2: Strange, the query in question doesn't even get the new results with UPDATE3: The difference is fragments (for the query where |
This PR is our first attempt to solve the
updateQueries
regression introduced byapollo-client
version0.5.22
in a bug fix while still trying to keep the bug that got fixed, well, fixed.The issue is that when
updateQueries
or areducer
references data for a component which has been unmounted then there is no way to update data in the store at that location. So say you had an app where the user moved to a comment entry screen and your comments list component was unmounted. The user enters a comment and submits a mutation with anupdateQueries
that references the query for the unmounted comments list component. TheupdateQueries
function would not run in this environment because of our bug fix.Before
0.5.22
line 502 ofsrc/core/ObservableQuery.ts
did not remove theObservableQuery
fromQueryManager
even when it was added earlier along in the observable query’s lifecycle. This behavior was incorrect and caused apollographql/apollo-client#993. So we fixed it! However, as a side effect of not removing theObservableQuery
fromQueryManager
anyObservableQuery
s on unmounted components were updated. In short, the bug had a desirable side affect!This PR aims to mitigate the problem which caused apollographql/apollo-client#993 while still providing the desirable
updateQueries
behavior that many of our users expect. In order to strike this balance this PR implementsObservableQuery
recycling. Here’s how it works.When a component is unmounted, instead of throwing away the
ObservableQuery
, we put it in a recycler to keep it alive. You can think of it like putting theObservableQuery
in the fridge to be reheated later. When a new component mounts we take our savedObservableQuery
out of the fridge, add some new options, and use the old observable query again.Now for every container component in your app (you create one whenever you call
graphql
) there should only be oneObservableQuery
that lives forever assuming that you only render our container component once at a time. If you render your component more than once at a time that should still be fine.When
ObservableQuery
is in the recycler it will receive anyupdateQueries
andreducer
store changes like normal. The only difference is that no component will re-render.There are probably going to be a few bugs associated with this change. My assumption is that it will be much easier to smash those smaller bugs as they come up instead of letting this
updateQueries
bug live on. Those bugs will also likely be much smaller in scope.Here are a few of the issues which can provide context on the issue. I may be missing some:
updateQueries
behavior. Old reducers still re-run apollo-client#993updateQueries
(we determined that we needed a fix before we could implement the new design): Design for new updateQueries functionality apollo-client#1224updateQueries
only updates queries on components that are mounted apollo-client#1211Basically, there were a lot of bug reports 😊. We didn’t fix this sooner because we saw the bug fix that caused this regression as technically correct, and we’re working on moving towards
apollo-client
1.0.I just want to write one more test to assert that the
updateQueries
behavior is ok. (because that’s why this PR was opened!)