Skip to content

Commit 8f3bc9b

Browse files
authored
Ensure variables are kept when refetching with a suspense hook that uses skipToken (#12993)
1 parent 9209396 commit 8f3bc9b

File tree

18 files changed

+1296
-508
lines changed

18 files changed

+1296
-508
lines changed

.changeset/eleven-bikes-flash.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+
Fix an issue where switching from options with `variables` to `skipToken` with `useSuspenseQuery` and `useBackgroundQuery` would create a new `ObservableQuery`. This could cause unintended refetches where `variables` were absent in the request when the query was referenced with `refetchQueries`.

.size-limits.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43812,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38745,
4-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33456,
5-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27523
2+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43882,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38754,
4+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33430,
5+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27518
66
}

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ For up to date release notes, refer to the project's [Changelog](https://github.
1818
### Apollo Client
1919

2020
#### 4.1.0
21+
2122
_Release candidate - November 14th, 2025_
2223

2324
- Support for `@stream`

config/jest.config.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,23 @@ const ignoreDTSFiles = ".d.ts$";
3939
const ignoreTSFiles = ".ts$";
4040
const ignoreTSXFiles = ".tsx$";
4141

42-
const react19TestFileIgnoreList = [ignoreDTSFiles, ignoreTSFiles];
43-
44-
const react17TestFileIgnoreList = [
42+
const reactSharedTestFileIgnoreList = [
4543
ignoreDTSFiles,
4644
ignoreTSFiles,
45+
"src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx",
46+
"src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx",
47+
];
48+
49+
const react17TestFileIgnoreList = [
50+
...reactSharedTestFileIgnoreList,
4751
// We only support Suspense with React 18, so don't test suspense hooks with
4852
// React 17
4953
"src/testing/experimental/__tests__/createTestSchema.test.tsx",
5054
"src/react/hooks/__tests__/useSuspenseFragment.test.tsx",
5155
"src/react/hooks/__tests__/useSuspenseQuery.test.tsx",
56+
"src/react/hooks/__tests__/useSuspenseQuery/*",
5257
"src/react/hooks/__tests__/useBackgroundQuery.test.tsx",
58+
"src/react/hooks/__tests__/useBackgroundQuery/*",
5359
"src/react/hooks/__tests__/useLoadableQuery.test.tsx",
5460
"src/react/hooks/__tests__/useQueryRefHandlers.test.tsx",
5561
"src/react/query-preloader/__tests__/createQueryPreloader.test.tsx",
@@ -81,15 +87,14 @@ const tsRxJSMinConfig = {
8187
const standardReact19Config = {
8288
...defaults,
8389
displayName: "ReactDOM 19",
84-
testPathIgnorePatterns: react19TestFileIgnoreList,
90+
testPathIgnorePatterns: reactSharedTestFileIgnoreList,
8591
};
8692

8793
const standardReact18Config = {
8894
...defaults,
8995
displayName: "ReactDOM 18",
9096
testPathIgnorePatterns: [
91-
ignoreDTSFiles,
92-
ignoreTSFiles,
97+
...reactSharedTestFileIgnoreList,
9398
"src/react/ssr/__tests__/prerenderStatic.test.tsx",
9499
],
95100
moduleNameMapper: {

package-lock.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@
180180
"@testing-library/dom": "10.4.0",
181181
"@testing-library/jest-dom": "6.6.3",
182182
"@testing-library/react": "16.1.0",
183-
"@testing-library/react-render-stream": "2.0.0",
183+
"@testing-library/react-render-stream": "2.0.2",
184184
"@testing-library/user-event": "14.5.2",
185185
"@types/babel__preset-env": "^7.10.0",
186186
"@types/bytes": "3.1.4",
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { disableActEnvironment } from "@testing-library/react-render-stream";
2+
import React from "react";
3+
import { delay, of } from "rxjs";
4+
5+
import {
6+
ApolloClient,
7+
ApolloLink,
8+
InMemoryCache,
9+
NetworkStatus,
10+
} from "@apollo/client";
11+
import { skipToken, useBackgroundQuery } from "@apollo/client/react";
12+
import {
13+
createClientWrapper,
14+
createMockWrapper,
15+
setupVariablesCase,
16+
} from "@apollo/client/testing/internal";
17+
18+
import { renderUseBackgroundQuery } from "./testUtils.js";
19+
20+
// https://github.com/apollographql/apollo-client/issues/12989
21+
test("maintains variables when switching to `skipToken` and calling `refetchQueries` while skipped after initial request", async () => {
22+
const { query } = setupVariablesCase();
23+
24+
const client = new ApolloClient({
25+
link: new ApolloLink((operation) => {
26+
return of(
27+
operation.variables.id === "1" ?
28+
{
29+
data: {
30+
character: {
31+
__typename: "Character",
32+
id: "1",
33+
name: "Spider-Man",
34+
},
35+
},
36+
}
37+
: {
38+
data: null,
39+
errors: [
40+
{ message: `Fetched wrong id: ${operation.variables.id}` },
41+
],
42+
}
43+
).pipe(delay(10));
44+
}),
45+
cache: new InMemoryCache(),
46+
});
47+
48+
using _disabledAct = disableActEnvironment();
49+
const { rerender, takeRender } = await renderUseBackgroundQuery(
50+
({ id }) =>
51+
useBackgroundQuery(
52+
query,
53+
id === undefined ? skipToken : { variables: { id } }
54+
),
55+
{
56+
initialProps: { id: "1" as string | undefined },
57+
wrapper: createClientWrapper(client),
58+
}
59+
);
60+
61+
{
62+
const { renderedComponents } = await takeRender();
63+
64+
expect(renderedComponents).toStrictEqual([
65+
"useBackgroundQuery",
66+
"<Suspense />",
67+
]);
68+
}
69+
70+
{
71+
const { snapshot, renderedComponents } = await takeRender();
72+
73+
expect(renderedComponents).toStrictEqual(["useReadQuery"]);
74+
expect(snapshot).toStrictEqualTyped({
75+
data: {
76+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
77+
},
78+
dataState: "complete",
79+
error: undefined,
80+
networkStatus: NetworkStatus.ready,
81+
});
82+
}
83+
84+
await rerender({ id: undefined });
85+
86+
{
87+
const { snapshot, renderedComponents } = await takeRender();
88+
89+
expect(renderedComponents).toStrictEqual([
90+
"useBackgroundQuery",
91+
"useReadQuery",
92+
]);
93+
expect(snapshot).toStrictEqualTyped({
94+
data: {
95+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
96+
},
97+
dataState: "complete",
98+
error: undefined,
99+
networkStatus: NetworkStatus.ready,
100+
});
101+
}
102+
103+
await expect(takeRender).not.toRerender();
104+
105+
await expect(
106+
client.refetchQueries({ include: [query] })
107+
).resolves.toStrictEqualTyped([
108+
{
109+
data: {
110+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
111+
},
112+
},
113+
]);
114+
115+
await expect(takeRender).not.toRerender();
116+
});
117+
118+
test("suspends and fetches when changing variables when no longer using skipToken", async () => {
119+
const { query, mocks } = setupVariablesCase({
120+
delay: React.version.startsWith("18") ? 200 : 20,
121+
});
122+
123+
using _disabledAct = disableActEnvironment();
124+
const { rerender, takeRender } = await renderUseBackgroundQuery(
125+
({ id }) =>
126+
useBackgroundQuery(
127+
query,
128+
id === undefined ? skipToken : { variables: { id } }
129+
),
130+
{
131+
initialProps: { id: "1" as string | undefined },
132+
wrapper: createMockWrapper({ mocks }),
133+
}
134+
);
135+
136+
{
137+
const { renderedComponents } = await takeRender();
138+
139+
expect(renderedComponents).toStrictEqual([
140+
"useBackgroundQuery",
141+
"<Suspense />",
142+
]);
143+
}
144+
145+
{
146+
const { snapshot, renderedComponents } = await takeRender();
147+
148+
expect(renderedComponents).toStrictEqual(["useReadQuery"]);
149+
expect(snapshot).toStrictEqualTyped({
150+
data: {
151+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
152+
},
153+
dataState: "complete",
154+
error: undefined,
155+
networkStatus: NetworkStatus.ready,
156+
});
157+
}
158+
159+
await rerender({ id: undefined });
160+
161+
{
162+
const { snapshot, renderedComponents } = await takeRender();
163+
164+
expect(renderedComponents).toStrictEqual([
165+
"useBackgroundQuery",
166+
"useReadQuery",
167+
]);
168+
expect(snapshot).toStrictEqualTyped({
169+
data: {
170+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
171+
},
172+
dataState: "complete",
173+
error: undefined,
174+
networkStatus: NetworkStatus.ready,
175+
});
176+
}
177+
178+
await rerender({ id: "2" });
179+
180+
{
181+
const { renderedComponents } = await takeRender();
182+
183+
expect(renderedComponents).toStrictEqual([
184+
"useBackgroundQuery",
185+
"<Suspense />",
186+
]);
187+
}
188+
189+
{
190+
const { snapshot } = await takeRender();
191+
192+
expect(snapshot).toStrictEqualTyped({
193+
data: {
194+
character: { __typename: "Character", id: "2", name: "Black Widow" },
195+
},
196+
dataState: "complete",
197+
error: undefined,
198+
networkStatus: NetworkStatus.ready,
199+
});
200+
}
201+
202+
await expect(takeRender).not.toRerender();
203+
});
204+
205+
test("does not suspend for data in the cache when changing variables when no longer using skipToken", async () => {
206+
const { query, mocks } = setupVariablesCase();
207+
208+
const cache = new InMemoryCache();
209+
210+
cache.writeQuery({
211+
query,
212+
data: {
213+
character: { __typename: "Character", id: "2", name: "Cached Widow" },
214+
},
215+
variables: { id: "2" },
216+
});
217+
218+
using _disabledAct = disableActEnvironment();
219+
const { rerender, takeRender } = await renderUseBackgroundQuery(
220+
({ id }) =>
221+
useBackgroundQuery(
222+
query,
223+
id === undefined ? skipToken : { variables: { id } }
224+
),
225+
{
226+
initialProps: { id: "1" as string | undefined },
227+
wrapper: createMockWrapper({ cache, mocks }),
228+
}
229+
);
230+
231+
{
232+
const { renderedComponents } = await takeRender();
233+
234+
expect(renderedComponents).toStrictEqual([
235+
"useBackgroundQuery",
236+
"<Suspense />",
237+
]);
238+
}
239+
240+
{
241+
const { snapshot, renderedComponents } = await takeRender();
242+
243+
expect(renderedComponents).toStrictEqual(["useReadQuery"]);
244+
expect(snapshot).toStrictEqualTyped({
245+
data: {
246+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
247+
},
248+
dataState: "complete",
249+
error: undefined,
250+
networkStatus: NetworkStatus.ready,
251+
});
252+
}
253+
254+
await rerender({ id: undefined });
255+
256+
{
257+
const { snapshot, renderedComponents } = await takeRender();
258+
259+
expect(renderedComponents).toStrictEqual([
260+
"useBackgroundQuery",
261+
"useReadQuery",
262+
]);
263+
expect(snapshot).toStrictEqualTyped({
264+
data: {
265+
character: { __typename: "Character", id: "1", name: "Spider-Man" },
266+
},
267+
dataState: "complete",
268+
error: undefined,
269+
networkStatus: NetworkStatus.ready,
270+
});
271+
}
272+
273+
await rerender({ id: "2" });
274+
275+
{
276+
const { snapshot, renderedComponents } = await takeRender();
277+
278+
expect(renderedComponents).toStrictEqual([
279+
"useBackgroundQuery",
280+
"useReadQuery",
281+
]);
282+
expect(snapshot).toStrictEqualTyped({
283+
data: {
284+
character: { __typename: "Character", id: "2", name: "Cached Widow" },
285+
},
286+
dataState: "complete",
287+
error: undefined,
288+
networkStatus: NetworkStatus.ready,
289+
});
290+
}
291+
292+
await expect(takeRender).not.toRerender();
293+
});

0 commit comments

Comments
 (0)