-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Fixed useQuery under StrictMode creating 2 observable query #11925
base: main
Are you sure you want to change the base?
Changes from 8 commits
823b26e
0db43d3
1e23e8e
4c71b46
7edb03f
28dd4e3
8a7a539
1500206
e568c78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@apollo/client": patch | ||
--- | ||
|
||
Fixed issue under StrictMode causing queries created with useQuery to be potentially refetched after unmounting. |
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -415,6 +415,17 @@ export class ApolloClient<TCacheShape> implements DataProxy { | |||||||||||||||
T = any, | ||||||||||||||||
TVariables extends OperationVariables = OperationVariables, | ||||||||||||||||
>(options: WatchQueryOptions<TVariables, T>): ObservableQuery<T, TVariables> { | ||||||||||||||||
const { observable, register } = this.createQuery(options); | ||||||||||||||||
register(); | ||||||||||||||||
return observable; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public createQuery< | ||||||||||||||||
T = any, | ||||||||||||||||
TVariables extends OperationVariables = OperationVariables, | ||||||||||||||||
>( | ||||||||||||||||
options: WatchQueryOptions<TVariables, T> | ||||||||||||||||
): { observable: ObservableQuery<T, TVariables>; register: () => void } { | ||||||||||||||||
if (this.defaultOptions.watchQuery) { | ||||||||||||||||
options = mergeOptions(this.defaultOptions.watchQuery, options); | ||||||||||||||||
} | ||||||||||||||||
|
@@ -428,7 +439,16 @@ export class ApolloClient<TCacheShape> implements DataProxy { | |||||||||||||||
options = { ...options, fetchPolicy: "cache-first" }; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
return this.queryManager.watchQuery<T, TVariables>(options); | ||||||||||||||||
const { observable, queryInfo } = this.queryManager.createQuery< | ||||||||||||||||
T, | ||||||||||||||||
TVariables | ||||||||||||||||
>(options); | ||||||||||||||||
return { | ||||||||||||||||
observable, | ||||||||||||||||
register: () => { | ||||||||||||||||
this.queryManager.registerObservableQuery(observable, queryInfo); | ||||||||||||||||
}, | ||||||||||||||||
}; | ||||||||||||||||
Comment on lines
+446
to
+451
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the suggested change to
Suggested change
|
||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -287,11 +287,17 @@ export class QueryInfo { | |
} | ||
|
||
private shouldNotify() { | ||
if (!this.dirty || !this.listeners.size) { | ||
if ( | ||
!this.dirty || | ||
!this.listeners.size || | ||
// It's possible that the query is no longer being watched, but the | ||
// ObservableQuery is still active/pending cleanup. In this case, we should not notify. | ||
!this.observableQuery?.hasObservers() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we probably want to call this change out in the changelog in case we have any users that use the core
Obviously feel free to workshop this and make it your own, really as long as we have something here is what I care about. Feel free to add this as a separate changeset since I'd view it as a separate change from the strict mode issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After talking with the team, I think we might want to move this change to 4.0 and make it a breaking change so that we can call it out prominently. Its difficult to tell who might rely on this behavior if you're using the core API directly. I think the fix for strict mode might be enough here to address what you're seeing specifically in your app. Moving this to a major allows us to ensure we add this behavior everywhere as well. As an FYI, we will begin work on 4.0 once 3.12 ships (a few weeks from now), so shouldn't be long before we can address this 🙂 |
||
) { | ||
return false; | ||
} | ||
|
||
if (isNetworkRequestInFlight(this.networkStatus) && this.observableQuery) { | ||
if (isNetworkRequestInFlight(this.networkStatus)) { | ||
const { fetchPolicy } = this.observableQuery.options; | ||
if (fetchPolicy !== "cache-only" && fetchPolicy !== "cache-and-network") { | ||
return false; | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -720,10 +720,16 @@ export class QueryManager<TStore> { | |||||||||||||||||||||||
}; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public watchQuery< | ||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* Create a query, but do not associate it with the QueryManager. | ||||||||||||||||||||||||
* This allows to throw away the query if it ends up not being needed | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
Comment on lines
+723
to
+726
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally fine if you want to keep this comment, but just an FYI we consider |
||||||||||||||||||||||||
public createQuery< | ||||||||||||||||||||||||
T, | ||||||||||||||||||||||||
TVariables extends OperationVariables = OperationVariables, | ||||||||||||||||||||||||
>(options: WatchQueryOptions<TVariables, T>): ObservableQuery<T, TVariables> { | ||||||||||||||||||||||||
>( | ||||||||||||||||||||||||
options: WatchQueryOptions<TVariables, T> | ||||||||||||||||||||||||
): { observable: ObservableQuery<T, TVariables>; queryInfo: QueryInfo } { | ||||||||||||||||||||||||
const query = this.transform(options.query); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// assign variable default values if supplied | ||||||||||||||||||||||||
|
@@ -746,8 +752,6 @@ export class QueryManager<TStore> { | |||||||||||||||||||||||
}); | ||||||||||||||||||||||||
observable["lastQuery"] = query; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
this.queries.set(observable.queryId, queryInfo); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// We give queryInfo the transformed query to ensure the first cache diff | ||||||||||||||||||||||||
// uses the transformed query instead of the raw query | ||||||||||||||||||||||||
queryInfo.init({ | ||||||||||||||||||||||||
|
@@ -756,6 +760,25 @@ export class QueryManager<TStore> { | |||||||||||||||||||||||
variables: observable.variables, | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return { observable, queryInfo }; | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the change suggested for
Suggested change
|
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* Register an ObservableQuery with the QueryManager created previously with createQuery. | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
public registerObservableQuery( | ||||||||||||||||||||||||
observable: ObservableQuery<any, any>, | ||||||||||||||||||||||||
queryInfo: QueryInfo | ||||||||||||||||||||||||
): void { | ||||||||||||||||||||||||
this.queries.set(observable.queryId, queryInfo); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+769
to
+774
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
When we instantiate |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
public watchQuery< | ||||||||||||||||||||||||
T, | ||||||||||||||||||||||||
TVariables extends OperationVariables = OperationVariables, | ||||||||||||||||||||||||
>(options: WatchQueryOptions<TVariables, T>): ObservableQuery<T, TVariables> { | ||||||||||||||||||||||||
const { observable, queryInfo } = this.createQuery(options); | ||||||||||||||||||||||||
this.registerObservableQuery(observable, queryInfo); | ||||||||||||||||||||||||
return observable; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4712,6 +4712,51 @@ describe("QueryManager", () => { | |
.then(resolve, reject); | ||
} | ||
); | ||
|
||
itAsync( | ||
"will not update inactive query on `resetStore` nor on `refetchQueries`", | ||
(resolve, reject) => { | ||
const testQuery = gql` | ||
query TestQuery { | ||
author { | ||
firstName | ||
lastName | ||
} | ||
} | ||
`; | ||
const link = new (class extends ApolloLink { | ||
public request() { | ||
reject(new Error("Query was not supposed to be called")); | ||
return null; | ||
} | ||
})(); | ||
|
||
const queryManager = new QueryManager( | ||
getDefaultOptionsForQueryManagerTests({ | ||
link, | ||
cache: new InMemoryCache({ addTypename: false }), | ||
}) | ||
); | ||
const oq = queryManager.watchQuery({ | ||
query: testQuery, | ||
fetchPolicy: "cache-and-network", | ||
}); | ||
// Recreate state where an observable query is dirty but has no observers in the query manager | ||
// @ts-expect-error -- Accessing private field for testing | ||
oq.queryInfo.dirty = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious, now that we've sorta pinned this down on both strict mode and the creation of multiple |
||
|
||
resetStore(queryManager).then((q) => { | ||
expect(q).toHaveLength(0); | ||
expect(oq.hasObservers()).toBe(false); | ||
resolve(); | ||
}); | ||
|
||
const refetched = queryManager.refetchQueries({ | ||
include: ["TestQuery"], | ||
}); | ||
expect(refetched.size).toBe(0); | ||
} | ||
); | ||
}); | ||
|
||
describe("loading state", () => { | ||
|
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'd make this a
private
method. We treat public methods as part of the public API which means we can't make breaking changes to this after this is released and I'd prefer we keep this out of users hands if we can. By making it private, that should allow us to tweak it/rename it/etc. without that fear.Since you use this from
useQuery
, TypeScript will obviously complain when you make this change, so to get around that, use bracket notation: