From 228429a1d36eae691473b24fb641ec3cd84c8a3d Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 8 Jul 2024 11:26:14 +0200 Subject: [PATCH] Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` option specified. (#11626) * Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` specified. fixes #11365 * update size-limits * remove `.only` * Clean up Prettier, Size-limit, and Api-Extractor * use `mockFetchQuery` helper in test * fix detail in test-tsconfig.json --------- Co-authored-by: phryneas --- .changeset/tasty-chairs-dress.md | 5 + .size-limits.json | 4 +- src/core/ObservableQuery.ts | 5 +- src/react/hooks/__tests__/useQuery.test.tsx | 127 ++++++++++++++++++++ src/tsconfig.json | 2 + 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 .changeset/tasty-chairs-dress.md diff --git a/.changeset/tasty-chairs-dress.md b/.changeset/tasty-chairs-dress.md new file mode 100644 index 00000000000..459c72bd44b --- /dev/null +++ b/.changeset/tasty-chairs-dress.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` specified. (fixes #11365) diff --git a/.size-limits.json b/.size-limits.json index 5cca69e7256..c9a1233d358 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39906, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32896 + "dist/apollo-client.min.cjs": 39924, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index f9e6dd6b1e4..7a419ff078e 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -910,7 +910,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, options.fetchPolicy !== "standby" && // If we're changing the fetchPolicy anyway, don't try to change it here // using applyNextFetchPolicy. The explicit options.fetchPolicy wins. - options.fetchPolicy === oldFetchPolicy + (options.fetchPolicy === oldFetchPolicy || + // A `nextFetchPolicy` function has even higher priority, though, + // so in that case `applyNextFetchPolicy` must be called. + typeof options.nextFetchPolicy === "function") ) { this.applyNextFetchPolicy("variables-changed", options); if (newNetworkStatus === void 0) { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 7909f8673d8..19a1ba57687 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -11,6 +11,7 @@ import { OperationVariables, TypedDocumentNode, WatchQueryFetchPolicy, + WatchQueryOptions, } from "../../../core"; import { InMemoryCache } from "../../../cache"; import { ApolloProvider } from "../../context"; @@ -36,6 +37,7 @@ import { } from "../../../testing/internal"; import { useApolloClient } from "../useApolloClient"; import { useLazyQuery } from "../useLazyQuery"; +import { mockFetchQuery } from "../../../core/__tests__/ObservableQuery"; const IS_REACT_17 = React.version.startsWith("17"); @@ -7071,6 +7073,131 @@ describe("useQuery Hook", () => { expect(reasons).toEqual(["variables-changed", "after-fetch"]); }); + + it("should prioritize a `nextFetchPolicy` function over a `fetchPolicy` option when changing variables", async () => { + const query = gql` + { + hello + } + `; + const link = new MockLink([ + { + request: { query, variables: { id: 1 } }, + result: { data: { hello: "from link" } }, + delay: 10, + }, + { + request: { query, variables: { id: 2 } }, + result: { data: { hello: "from link2" } }, + delay: 10, + }, + ]); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); + + const mocks = mockFetchQuery(client["queryManager"]); + + const expectQueryTriggered = ( + nth: number, + fetchPolicy: WatchQueryFetchPolicy + ) => { + expect(mocks.fetchQueryByPolicy).toHaveBeenCalledTimes(nth); + expect(mocks.fetchQueryByPolicy).toHaveBeenNthCalledWith( + nth, + expect.anything(), + expect.objectContaining({ fetchPolicy }), + expect.any(Number) + ); + }; + let nextFetchPolicy: WatchQueryOptions< + OperationVariables, + any + >["nextFetchPolicy"] = (_, context) => { + if (context.reason === "variables-changed") { + return "cache-and-network"; + } else if (context.reason === "after-fetch") { + return "cache-only"; + } + throw new Error("should never happen"); + }; + nextFetchPolicy = jest.fn(nextFetchPolicy); + + const { result, rerender } = renderHook< + QueryResult, + { + variables: { id: number }; + } + >( + ({ variables }) => + useQuery(query, { + fetchPolicy: "network-only", + variables, + notifyOnNetworkStatusChange: true, + nextFetchPolicy, + }), + { + initialProps: { + variables: { id: 1 }, + }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + // first network request triggers with initial fetchPolicy + expectQueryTriggered(1, "network-only"); + + await waitFor(() => { + expect(result.current.networkStatus).toBe(NetworkStatus.ready); + }); + + expect(nextFetchPolicy).toHaveBeenCalledTimes(1); + expect(nextFetchPolicy).toHaveBeenNthCalledWith( + 1, + "network-only", + expect.objectContaining({ + reason: "after-fetch", + }) + ); + // `nextFetchPolicy(..., {reason: "after-fetch"})` changed it to + // cache-only + expect(result.current.observable.options.fetchPolicy).toBe("cache-only"); + + rerender({ + variables: { id: 2 }, + }); + + expect(nextFetchPolicy).toHaveBeenNthCalledWith( + 2, + // has been reset to the initial `fetchPolicy` of "network-only" because + // we changed variables, then `nextFetchPolicy` is called + "network-only", + expect.objectContaining({ + reason: "variables-changed", + }) + ); + // the return value of `nextFetchPolicy(..., {reason: "variables-changed"})` + expectQueryTriggered(2, "cache-and-network"); + + await waitFor(() => { + expect(result.current.networkStatus).toBe(NetworkStatus.ready); + }); + + expect(nextFetchPolicy).toHaveBeenCalledTimes(3); + expect(nextFetchPolicy).toHaveBeenNthCalledWith( + 3, + "cache-and-network", + expect.objectContaining({ + reason: "after-fetch", + }) + ); + // `nextFetchPolicy(..., {reason: "after-fetch"})` changed it to + // cache-only + expect(result.current.observable.options.fetchPolicy).toBe("cache-only"); + }); }); describe("Missing Fields", () => { diff --git a/src/tsconfig.json b/src/tsconfig.json index efeb2f2da38..d7e90510ecc 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,6 +5,8 @@ { "compilerOptions": { "noEmit": true, + "declaration": false, + "declarationMap": false, "lib": ["es2015", "esnext.asynciterable", "ES2021.WeakRef"], "types": ["jest", "node", "./testing/matchers/index.d.ts"] },