Skip to content

Commit 699c830

Browse files
authored
Ensure a refetch that changes variables and returns deep equal results rerenders (#12729)
1 parent 07a0c8c commit 699c830

File tree

3 files changed

+252
-3
lines changed

3 files changed

+252
-3
lines changed

.changeset/tough-olives-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Ensure `useQuery` rerenders when `notifyOnNetworkStatusChange` is `false` and a `refetch` that changes variables returns a result deeply equal to previous variables.

src/react/hooks/__tests__/useQuery.test.tsx

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13026,6 +13026,234 @@ test("renders loading states at appropriate times on next fetch after updating `
1302613026
await expect(takeSnapshot).not.toRerender();
1302713027
});
1302813028

13029+
// https://github.com/apollographql/apollo-client/issues/11328
13030+
test("rerenders if refetch returns same result for different variables with notifyOnNetworkStatusChange: false", async () => {
13031+
type Data = typeof data;
13032+
type Vars = { first: number };
13033+
const query: TypedDocumentNode<Data, Vars> = gql`
13034+
query people($first: Int!) {
13035+
allPeople(first: $first) {
13036+
people {
13037+
name
13038+
friends(id: $first) {
13039+
name
13040+
}
13041+
}
13042+
}
13043+
}
13044+
`;
13045+
13046+
const variables1: Vars = { first: 1 };
13047+
const variables2: Vars = { first: 2 };
13048+
const data = {
13049+
allPeople: {
13050+
__typename: "AllPeople",
13051+
people: [
13052+
{
13053+
__typename: "Person",
13054+
name: "Luke Skywalker",
13055+
friends: [{ __typename: "Person", name: "r2d2" }],
13056+
},
13057+
],
13058+
},
13059+
};
13060+
13061+
const link = new MockLink([
13062+
{
13063+
request: { query, variables: variables1 },
13064+
result: { data },
13065+
maxUsageCount: Number.POSITIVE_INFINITY,
13066+
},
13067+
{
13068+
request: { query, variables: variables2 },
13069+
result: { data },
13070+
maxUsageCount: Number.POSITIVE_INFINITY,
13071+
},
13072+
]);
13073+
13074+
const client = new ApolloClient({
13075+
link,
13076+
cache: new InMemoryCache(),
13077+
});
13078+
13079+
using _disabledAct = disableActEnvironment();
13080+
const renderStream = await renderHookToSnapshotStream(
13081+
({ variables }) =>
13082+
useQuery(query, { variables, notifyOnNetworkStatusChange: false }),
13083+
{
13084+
initialProps: { variables: variables1 },
13085+
wrapper: ({ children }) => (
13086+
<ApolloProvider client={client}>{children}</ApolloProvider>
13087+
),
13088+
}
13089+
);
13090+
13091+
const { takeSnapshot, getCurrentSnapshot } = renderStream;
13092+
13093+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13094+
data: undefined,
13095+
dataState: "empty",
13096+
loading: true,
13097+
networkStatus: NetworkStatus.loading,
13098+
previousData: undefined,
13099+
variables: variables1,
13100+
});
13101+
13102+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13103+
data,
13104+
dataState: "complete",
13105+
loading: false,
13106+
networkStatus: NetworkStatus.ready,
13107+
previousData: undefined,
13108+
variables: variables1,
13109+
});
13110+
13111+
await expect(
13112+
getCurrentSnapshot().refetch(variables2)
13113+
).resolves.toStrictEqualTyped({ data });
13114+
13115+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13116+
data,
13117+
dataState: "complete",
13118+
loading: false,
13119+
networkStatus: NetworkStatus.ready,
13120+
previousData: undefined,
13121+
variables: variables2,
13122+
});
13123+
13124+
await expect(
13125+
getCurrentSnapshot().refetch(variables1)
13126+
).resolves.toStrictEqualTyped({ data });
13127+
13128+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13129+
data,
13130+
dataState: "complete",
13131+
loading: false,
13132+
networkStatus: NetworkStatus.ready,
13133+
previousData: undefined,
13134+
variables: variables1,
13135+
});
13136+
13137+
await expect(renderStream).not.toRerender();
13138+
});
13139+
13140+
test("rerenders if changing variables returns same result for different variables with notifyOnNetworkStatusChange: false", async () => {
13141+
type Data = typeof data;
13142+
type Vars = { first: number };
13143+
const query: TypedDocumentNode<Data, Vars> = gql`
13144+
query people($first: Int!) {
13145+
allPeople(first: $first) {
13146+
people {
13147+
name
13148+
friends(id: $first) {
13149+
name
13150+
}
13151+
}
13152+
}
13153+
}
13154+
`;
13155+
13156+
const variables1: Vars = { first: 1 };
13157+
const variables2: Vars = { first: 2 };
13158+
const data = {
13159+
allPeople: {
13160+
__typename: "AllPeople",
13161+
people: [
13162+
{
13163+
__typename: "Person",
13164+
name: "Luke Skywalker",
13165+
friends: [{ __typename: "Person", name: "r2d2" }],
13166+
},
13167+
],
13168+
},
13169+
};
13170+
13171+
const link = new MockLink([
13172+
{
13173+
request: { query, variables: variables1 },
13174+
result: { data },
13175+
maxUsageCount: Number.POSITIVE_INFINITY,
13176+
},
13177+
{
13178+
request: { query, variables: variables2 },
13179+
result: { data },
13180+
maxUsageCount: Number.POSITIVE_INFINITY,
13181+
},
13182+
]);
13183+
13184+
const client = new ApolloClient({
13185+
link,
13186+
cache: new InMemoryCache(),
13187+
});
13188+
13189+
using _disabledAct = disableActEnvironment();
13190+
const renderStream = await renderHookToSnapshotStream(
13191+
({ variables }) =>
13192+
useQuery(query, { variables, notifyOnNetworkStatusChange: false }),
13193+
{
13194+
initialProps: { variables: variables1 },
13195+
wrapper: ({ children }) => (
13196+
<ApolloProvider client={client}>{children}</ApolloProvider>
13197+
),
13198+
}
13199+
);
13200+
13201+
const { takeSnapshot, getCurrentSnapshot, rerender } = renderStream;
13202+
13203+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13204+
data: undefined,
13205+
dataState: "empty",
13206+
loading: true,
13207+
networkStatus: NetworkStatus.loading,
13208+
previousData: undefined,
13209+
variables: variables1,
13210+
});
13211+
13212+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13213+
data,
13214+
dataState: "complete",
13215+
loading: false,
13216+
networkStatus: NetworkStatus.ready,
13217+
previousData: undefined,
13218+
variables: variables1,
13219+
});
13220+
13221+
await rerender({ variables: variables2 });
13222+
13223+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13224+
data: undefined,
13225+
dataState: "empty",
13226+
loading: true,
13227+
networkStatus: NetworkStatus.setVariables,
13228+
previousData: data,
13229+
variables: variables2,
13230+
});
13231+
13232+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13233+
data,
13234+
dataState: "complete",
13235+
loading: false,
13236+
networkStatus: NetworkStatus.ready,
13237+
previousData: data,
13238+
variables: variables2,
13239+
});
13240+
13241+
await expect(
13242+
getCurrentSnapshot().refetch(variables1)
13243+
).resolves.toStrictEqualTyped({ data });
13244+
13245+
await expect(takeSnapshot()).resolves.toStrictEqualTyped({
13246+
data,
13247+
dataState: "complete",
13248+
loading: false,
13249+
networkStatus: NetworkStatus.ready,
13250+
previousData: data,
13251+
variables: variables1,
13252+
});
13253+
13254+
await expect(renderStream).not.toRerender();
13255+
});
13256+
1302913257
describe.skip("Type Tests", () => {
1303013258
test("returns narrowed TData in default case", () => {
1303113259
const { query } = setupSimpleCase();

src/react/hooks/useQuery.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ interface InternalResult<TData> {
174174
// okay/normal for them to be initially undefined.
175175
current: ApolloQueryResult<TData>;
176176
previousData?: undefined | MaybeMasked<TData>;
177+
178+
// Track current variables separately in case a call to e.g. `refetch(newVars)`
179+
// causes an emit that is deeply equal to the current result. This lets us
180+
// compare if we should force rerender due to changed variables
181+
variables: OperationVariables;
177182
}
178183

179184
interface InternalState<TData, TVariables extends OperationVariables> {
@@ -311,6 +316,7 @@ function useQuery_<TData, TVariables extends OperationVariables>(
311316
// Reuse previousData from previous InternalState (if any) to provide
312317
// continuity of previousData even if/when the query or client changes.
313318
previousData: previous?.resultData.current.data as TData,
319+
variables: observable.variables,
314320
},
315321
};
316322
}
@@ -405,13 +411,22 @@ function useResult<TData, TVariables extends OperationVariables>(
405411
.pipe(observeOn(asapScheduler))
406412
.subscribe((result) => {
407413
const previous = resultData.current;
408-
// Make sure we're not attempting to re-render similar results
409-
if (equal(previous, result)) {
414+
415+
if (
416+
// Avoid rerendering if the result is the same
417+
equal(previous, result) &&
418+
// Force rerender if the value was emitted because variables
419+
// changed, such as when calling `refetch(newVars)` which returns
420+
// the same data when `notifyOnNetworkStatusChange` is `false`.
421+
equal(resultData.variables, observable.variables)
422+
) {
410423
return;
411424
}
412425

426+
// eslint-disable-next-line react-compiler/react-compiler
427+
resultData.variables = observable.variables;
428+
413429
if (previous.data && !equal(previous.data, result.data)) {
414-
// eslint-disable-next-line react-compiler/react-compiler
415430
resultData.previousData = previous.data as TData;
416431
}
417432

@@ -476,6 +491,7 @@ function useResubscribeIfNecessary<
476491
(resultData.previousData as TData)) as TData;
477492
}
478493
resultData.current = result;
494+
resultData.variables = observable.variables;
479495
}
480496
observable[lastWatchOptions] = watchQueryOptions;
481497
}

0 commit comments

Comments
 (0)