diff --git a/.changeset/smooth-spoons-cough.md b/.changeset/smooth-spoons-cough.md
new file mode 100644
index 00000000000..dd5dc48bde1
--- /dev/null
+++ b/.changeset/smooth-spoons-cough.md
@@ -0,0 +1,5 @@
+---
+"@apollo/client": patch
+---
+
+Fix an issue where a polled query created in React strict mode may not stop polling after the component unmounts while using the `cache-and-network` fetch policy.
diff --git a/.size-limits.json b/.size-limits.json
index 7be0182bcaf..05c84cd4f6a 100644
--- a/.size-limits.json
+++ b/.size-limits.json
@@ -1,4 +1,4 @@
{
- "dist/apollo-client.min.cjs": 39577,
- "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32827
+ "dist/apollo-client.min.cjs": 39581,
+ "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32832
}
diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts
index 2b42f3b044e..47deef22483 100644
--- a/src/core/ObservableQuery.ts
+++ b/src/core/ObservableQuery.ts
@@ -781,7 +781,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,
options: { pollInterval },
} = this;
- if (!pollInterval) {
+ if (!pollInterval || !this.hasObservers()) {
if (pollingInfo) {
clearTimeout(pollingInfo.timeout);
delete this.pollingInfo;
diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx
index a176f88903a..268ca9f3134 100644
--- a/src/react/hooks/__tests__/useQuery.test.tsx
+++ b/src/react/hooks/__tests__/useQuery.test.tsx
@@ -22,6 +22,7 @@ import {
MockSubscriptionLink,
mockSingleLink,
tick,
+ wait,
} from "../../../testing";
import { QueryResult } from "../../types/types";
import { useQuery } from "../useQuery";
@@ -1887,6 +1888,86 @@ describe("useQuery Hook", () => {
requestSpy.mockRestore();
});
+ // https://github.com/apollographql/apollo-client/issues/9431
+ // https://github.com/apollographql/apollo-client/issues/11750
+ it("stops polling when component unmounts with cache-and-network fetch policy", async () => {
+ const query: TypedDocumentNode<{ hello: string }> = gql`
+ query {
+ hello
+ }
+ `;
+
+ const mocks = [
+ {
+ request: { query },
+ result: { data: { hello: "world 1" } },
+ },
+ {
+ request: { query },
+ result: { data: { hello: "world 2" } },
+ },
+ {
+ request: { query },
+ result: { data: { hello: "world 3" } },
+ },
+ ];
+
+ const cache = new InMemoryCache();
+
+ const link = new MockLink(mocks);
+ const requestSpy = jest.spyOn(link, "request");
+ const onErrorFn = jest.fn();
+ link.setOnError(onErrorFn);
+
+ const ProfiledHook = profileHook(() =>
+ useQuery(query, { pollInterval: 10, fetchPolicy: "cache-and-network" })
+ );
+
+ const client = new ApolloClient({
+ queryDeduplication: false,
+ link,
+ cache,
+ });
+
+ const { unmount } = render(, {
+ wrapper: ({ children }: any) => (
+ {children}
+ ),
+ });
+
+ {
+ const snapshot = await ProfiledHook.takeSnapshot();
+
+ expect(snapshot.loading).toBe(true);
+ expect(snapshot.data).toBeUndefined();
+ }
+
+ {
+ const snapshot = await ProfiledHook.takeSnapshot();
+
+ expect(snapshot.loading).toBe(false);
+ expect(snapshot.data).toEqual({ hello: "world 1" });
+ expect(requestSpy).toHaveBeenCalledTimes(1);
+ }
+
+ await wait(10);
+
+ {
+ const snapshot = await ProfiledHook.takeSnapshot();
+
+ expect(snapshot.loading).toBe(false);
+ expect(snapshot.data).toEqual({ hello: "world 2" });
+ expect(requestSpy).toHaveBeenCalledTimes(2);
+ }
+
+ unmount();
+
+ await expect(ProfiledHook).not.toRerender({ timeout: 50 });
+
+ expect(requestSpy).toHaveBeenCalledTimes(2);
+ expect(onErrorFn).toHaveBeenCalledTimes(0);
+ });
+
it("should stop polling when component is unmounted in Strict Mode", async () => {
const query = gql`
{
@@ -1960,6 +2041,84 @@ describe("useQuery Hook", () => {
requestSpy.mockRestore();
});
+ // https://github.com/apollographql/apollo-client/issues/9431
+ // https://github.com/apollographql/apollo-client/issues/11750
+ it("stops polling when component unmounts in strict mode with cache-and-network fetch policy", async () => {
+ const query: TypedDocumentNode<{ hello: string }> = gql`
+ query {
+ hello
+ }
+ `;
+
+ const mocks = [
+ {
+ request: { query },
+ result: { data: { hello: "world 1" } },
+ },
+ {
+ request: { query },
+ result: { data: { hello: "world 2" } },
+ },
+ {
+ request: { query },
+ result: { data: { hello: "world 3" } },
+ },
+ ];
+
+ const cache = new InMemoryCache();
+
+ const link = new MockLink(mocks);
+ const requestSpy = jest.spyOn(link, "request");
+ const onErrorFn = jest.fn();
+ link.setOnError(onErrorFn);
+
+ const ProfiledHook = profileHook(() =>
+ useQuery(query, { pollInterval: 10, fetchPolicy: "cache-and-network" })
+ );
+
+ const client = new ApolloClient({ link, cache });
+
+ const { unmount } = render(, {
+ wrapper: ({ children }: any) => (
+
+ {children}
+
+ ),
+ });
+
+ {
+ const snapshot = await ProfiledHook.takeSnapshot();
+
+ expect(snapshot.loading).toBe(true);
+ expect(snapshot.data).toBeUndefined();
+ }
+
+ {
+ const snapshot = await ProfiledHook.takeSnapshot();
+
+ expect(snapshot.loading).toBe(false);
+ expect(snapshot.data).toEqual({ hello: "world 1" });
+ expect(requestSpy).toHaveBeenCalledTimes(1);
+ }
+
+ await wait(10);
+
+ {
+ const snapshot = await ProfiledHook.takeSnapshot();
+
+ expect(snapshot.loading).toBe(false);
+ expect(snapshot.data).toEqual({ hello: "world 2" });
+ expect(requestSpy).toHaveBeenCalledTimes(2);
+ }
+
+ unmount();
+
+ await expect(ProfiledHook).not.toRerender({ timeout: 50 });
+
+ expect(requestSpy).toHaveBeenCalledTimes(2);
+ expect(onErrorFn).toHaveBeenCalledTimes(0);
+ });
+
it("should start and stop polling in Strict Mode", async () => {
const query = gql`
{