diff --git a/.changeset/tasty-hotels-press.md b/.changeset/tasty-hotels-press.md new file mode 100644 index 00000000000..c79a5db61d6 --- /dev/null +++ b/.changeset/tasty-hotels-press.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix issue where setting a default `watchQuery` option in the `ApolloClient` constructor could break `startTransition` when used with suspense hooks. diff --git a/.size-limits.json b/.size-limits.json index 2c7dbd56314..10e91f0ebf7 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39273, + "dist/apollo-client.min.cjs": 39277, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32630 } diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index adcc9f430d2..045f7221e7a 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -5249,6 +5249,210 @@ describe("fetchMore", () => { await expect(Profiler).not.toRerender(); }); + + // https://github.com/apollographql/apollo-client/issues/11708 + it("`fetchMore` works with startTransition when setting errorPolicy as default option in ApolloClient constructor", async () => { + type Variables = { + offset: number; + }; + + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; + + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), + }, + }, + }, + }), + defaultOptions: { + watchQuery: { + errorPolicy: "all", + }, + }, + }); + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query, { + variables: { offset: 0 }, + }); + + Profiler.mergeSnapshot({ isPending }); + + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Load more"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); + }); }); describe.skip("type tests", () => { diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 9a0e61fd55f..9fbbc524521 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -10109,6 +10109,225 @@ describe("useSuspenseQuery", () => { await expect(Profiler).not.toRerender(); }); + // https://github.com/apollographql/apollo-client/issues/11708 + it("`fetchMore` works with startTransition when setting errorPolicy as default option in ApolloClient constructor", async () => { + type Variables = { + offset: number; + }; + + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; + + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as Pick< + UseSuspenseQueryResult, + "data" | "error" | "networkStatus" + > | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + + return
Loading...
; + } + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), + }, + }, + }, + }), + defaultOptions: { + watchQuery: { + errorPolicy: "all", + }, + }, + }); + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const { data, error, networkStatus, fetchMore } = useSuspenseQuery( + query, + { + variables: { offset: 0 }, + } + ); + + Profiler.mergeSnapshot({ + isPending, + result: { data, error, networkStatus }, + }); + + return ( + + ); + } + + render(, { + wrapper: ({ children }) => ( + + + }>{children} + + + ), + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Load more"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); + }); + // https://github.com/apollographql/apollo-client/issues/11642 it("returns merged array when `fetchMore` returns empty array of results", async () => { const query: TypedDocumentNode = diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index 110a0823bb8..68efe9272b7 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -253,6 +253,7 @@ export class InternalQueryReference { didChangeOptions(watchQueryOptions: ObservedOptions) { return OBSERVED_CHANGED_OPTIONS.some( (option) => + option in watchQueryOptions && !equal(this.watchQueryOptions[option], watchQueryOptions[option]) ); }