Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
00725ac
Add failing test to reproduce issue
jerelmiller Oct 30, 2025
1e9819d
Fix issue where skipToken with refetchQueries would refetch empty var…
jerelmiller Oct 30, 2025
e58febc
Add test to ensure changing variables after skipToken suspends
jerelmiller Oct 30, 2025
498c4c8
Add test to ensure cached value is honored
jerelmiller Oct 30, 2025
1994e64
Swap order of tests
jerelmiller Oct 30, 2025
f7b0744
Add tests for skipToken with useSuspenseQuery
jerelmiller Oct 30, 2025
dfd7459
Add helper for useSuspenseQuery
jerelmiller Oct 30, 2025
25803f7
Use helper in new tests
jerelmiller Oct 30, 2025
dfaf2a8
Add fix to useSuspenseQuery
jerelmiller Oct 30, 2025
058b470
Remove unused imports
jerelmiller Oct 30, 2025
642648e
Move tests to own folder
jerelmiller Oct 30, 2025
a7fe575
Deprecate old renderSuspenseHook helper
jerelmiller Oct 30, 2025
9e151cc
Update tests to use render helper
jerelmiller Oct 30, 2025
04a757c
Rename file to testUtils
jerelmiller Oct 30, 2025
ae8b132
Add a testUtils file for useBackgroundQuery
jerelmiller Oct 30, 2025
e6855c5
Move new tests to own file
jerelmiller Oct 30, 2025
936789b
Allow custom delay for setupVariablesCase
jerelmiller Oct 30, 2025
961bc04
Fix issue in React 18 with tests
jerelmiller Oct 30, 2025
a91b1de
Fix ignore path
jerelmiller Oct 30, 2025
e84a075
Create shared ignore list
jerelmiller Oct 30, 2025
d541c26
Add longer delay to skipToken tests
jerelmiller Oct 30, 2025
68a4a22
Extract cache key to hook
jerelmiller Oct 30, 2025
c044493
Remove comment
jerelmiller Oct 30, 2025
738a714
Fix formatting
jerelmiller Oct 30, 2025
1db3a36
Add patch for type
jerelmiller Oct 30, 2025
90b6791
Rename helper functions for clarity
jerelmiller Oct 30, 2025
a9b484d
Rename component names in snapshots
jerelmiller Oct 30, 2025
bb25972
Update deprecation message
jerelmiller Oct 30, 2025
40eddcd
Adjust comment for typos and accuracy
jerelmiller Oct 30, 2025
b6f09c8
Add changeset
jerelmiller Oct 30, 2025
09e72fc
Update size limits
jerelmiller Oct 30, 2025
6ffc611
Upgrade react-render-stream and remove patch
jerelmiller Oct 31, 2025
17abb5c
Fix type issues after upgrade
jerelmiller Oct 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-bikes-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

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`.
8 changes: 4 additions & 4 deletions .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43812,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38745,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33456,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27523
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43882,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38754,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33430,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27518
}
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ For up to date release notes, refer to the project's [Changelog](https://github.
### Apollo Client

#### 4.1.0

_Release candidate - November 14th, 2025_

- Support for `@stream`
Expand Down
17 changes: 11 additions & 6 deletions config/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,23 @@ const ignoreDTSFiles = ".d.ts$";
const ignoreTSFiles = ".ts$";
const ignoreTSXFiles = ".tsx$";

const react19TestFileIgnoreList = [ignoreDTSFiles, ignoreTSFiles];

const react17TestFileIgnoreList = [
const reactSharedTestFileIgnoreList = [
ignoreDTSFiles,
ignoreTSFiles,
"src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx",
"src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx",
];

const react17TestFileIgnoreList = [
...reactSharedTestFileIgnoreList,
// We only support Suspense with React 18, so don't test suspense hooks with
// React 17
"src/testing/experimental/__tests__/createTestSchema.test.tsx",
"src/react/hooks/__tests__/useSuspenseFragment.test.tsx",
"src/react/hooks/__tests__/useSuspenseQuery.test.tsx",
"src/react/hooks/__tests__/useSuspenseQuery/*",
"src/react/hooks/__tests__/useBackgroundQuery.test.tsx",
"src/react/hooks/__tests__/useBackgroundQuery/*",
"src/react/hooks/__tests__/useLoadableQuery.test.tsx",
"src/react/hooks/__tests__/useQueryRefHandlers.test.tsx",
"src/react/query-preloader/__tests__/createQueryPreloader.test.tsx",
Expand Down Expand Up @@ -81,15 +87,14 @@ const tsRxJSMinConfig = {
const standardReact19Config = {
...defaults,
displayName: "ReactDOM 19",
testPathIgnorePatterns: react19TestFileIgnoreList,
testPathIgnorePatterns: reactSharedTestFileIgnoreList,
};

const standardReact18Config = {
...defaults,
displayName: "ReactDOM 18",
testPathIgnorePatterns: [
ignoreDTSFiles,
ignoreTSFiles,
...reactSharedTestFileIgnoreList,
"src/react/ssr/__tests__/prerenderStatic.test.tsx",
],
moduleNameMapper: {
Expand Down
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.1.0",
"@testing-library/react-render-stream": "2.0.0",
"@testing-library/react-render-stream": "2.0.2",
"@testing-library/user-event": "14.5.2",
"@types/babel__preset-env": "^7.10.0",
"@types/bytes": "3.1.4",
Expand Down
293 changes: 293 additions & 0 deletions src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import { disableActEnvironment } from "@testing-library/react-render-stream";
import React from "react";
import { delay, of } from "rxjs";

import {
ApolloClient,
ApolloLink,
InMemoryCache,
NetworkStatus,
} from "@apollo/client";
import { skipToken, useBackgroundQuery } from "@apollo/client/react";
import {
createClientWrapper,
createMockWrapper,
setupVariablesCase,
} from "@apollo/client/testing/internal";

import { renderUseBackgroundQuery } from "./testUtils.js";

// https://github.com/apollographql/apollo-client/issues/12989
test("maintains variables when switching to `skipToken` and calling `refetchQueries` while skipped after initial request", async () => {
const { query } = setupVariablesCase();

const client = new ApolloClient({
link: new ApolloLink((operation) => {
return of(
operation.variables.id === "1" ?
{
data: {
character: {
__typename: "Character",
id: "1",
name: "Spider-Man",
},
},
}
: {
data: null,
errors: [
{ message: `Fetched wrong id: ${operation.variables.id}` },
],
}
).pipe(delay(10));
}),
cache: new InMemoryCache(),
});

using _disabledAct = disableActEnvironment();
const { rerender, takeRender } = await renderUseBackgroundQuery(
({ id }) =>
useBackgroundQuery(
query,
id === undefined ? skipToken : { variables: { id } }
),
{
initialProps: { id: "1" as string | undefined },
wrapper: createClientWrapper(client),
}
);

{
const { renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"<Suspense />",
]);
}

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual(["useReadQuery"]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await rerender({ id: undefined });

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"useReadQuery",
]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await expect(takeRender).not.toRerender();

await expect(
client.refetchQueries({ include: [query] })
).resolves.toStrictEqualTyped([
{
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
},
]);

await expect(takeRender).not.toRerender();
});

test("suspends and fetches when changing variables when no longer using skipToken", async () => {
const { query, mocks } = setupVariablesCase({
delay: React.version.startsWith("18") ? 200 : 20,
});

using _disabledAct = disableActEnvironment();
const { rerender, takeRender } = await renderUseBackgroundQuery(
({ id }) =>
useBackgroundQuery(
query,
id === undefined ? skipToken : { variables: { id } }
),
{
initialProps: { id: "1" as string | undefined },
wrapper: createMockWrapper({ mocks }),
}
);

{
const { renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"<Suspense />",
]);
}

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual(["useReadQuery"]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await rerender({ id: undefined });

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"useReadQuery",
]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await rerender({ id: "2" });

{
const { renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"<Suspense />",
]);
}

{
const { snapshot } = await takeRender();

expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "2", name: "Black Widow" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await expect(takeRender).not.toRerender();
});

test("does not suspend for data in the cache when changing variables when no longer using skipToken", async () => {
const { query, mocks } = setupVariablesCase();

const cache = new InMemoryCache();

cache.writeQuery({
query,
data: {
character: { __typename: "Character", id: "2", name: "Cached Widow" },
},
variables: { id: "2" },
});

using _disabledAct = disableActEnvironment();
const { rerender, takeRender } = await renderUseBackgroundQuery(
({ id }) =>
useBackgroundQuery(
query,
id === undefined ? skipToken : { variables: { id } }
),
{
initialProps: { id: "1" as string | undefined },
wrapper: createMockWrapper({ cache, mocks }),
}
);

{
const { renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"<Suspense />",
]);
}

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual(["useReadQuery"]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await rerender({ id: undefined });

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"useReadQuery",
]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "1", name: "Spider-Man" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await rerender({ id: "2" });

{
const { snapshot, renderedComponents } = await takeRender();

expect(renderedComponents).toStrictEqual([
"useBackgroundQuery",
"useReadQuery",
]);
expect(snapshot).toStrictEqualTyped({
data: {
character: { __typename: "Character", id: "2", name: "Cached Widow" },
},
dataState: "complete",
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await expect(takeRender).not.toRerender();
});
Loading