From 00725ac8a5272a04f9fe87b48361030abd58328a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 11:10:49 -0600 Subject: [PATCH 01/33] Add failing test to reproduce issue --- .../__tests__/useBackgroundQuery.test.tsx | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 647d4ca3ff2..5fbadef37db 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -12,7 +12,7 @@ import { GraphQLError } from "graphql"; import React, { Suspense } from "react"; import type { FallbackProps } from "react-error-boundary"; import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; -import { Observable, of } from "rxjs"; +import { delay, Observable, of } from "rxjs"; import type { DataState, @@ -1910,6 +1910,109 @@ it("renders skip result, does not suspend, and maintains `data` when `skip` beco await expect(renderStream).not.toRerender({ timeout: 50 }); }); +// 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 user = userEvent.setup(); + const renderStream = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(renderStream); + + 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(), + }); + + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(false); + const [queryRef] = useBackgroundQuery( + query, + skip ? skipToken : { variables: { id: "1" } } + ); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + using _disabledAct = disableActEnvironment(); + await renderStream.render(, { wrapper: createClientWrapper(client) }); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await renderStream.takeRender(); + + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await user.click(screen.getByText("Toggle skip")); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender(); + + await expect( + client.refetchQueries({ include: [query] }) + ).resolves.toStrictEqualTyped([ + { + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + }, + ]); + + await expect(renderStream).not.toRerender(); +}); + it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { const { query, mocks } = setupSimpleCase(); const user = userEvent.setup(); From 1e9819db61a0e1f47bbe842bcc20c43d8f24e97e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 11:22:08 -0600 Subject: [PATCH 02/33] Fix issue where skipToken with refetchQueries would refetch empty variables --- src/react/hooks/useBackgroundQuery.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 6634b99adc4..79b82317e38 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -32,6 +32,7 @@ import type { } from "@apollo/client/utilities/internal"; import type { SkipToken } from "./constants.js"; +import { skipToken } from "./constants.js"; import { wrapHook } from "./internal/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; @@ -451,6 +452,21 @@ function useBackgroundQuery_< const watchQueryOptions = useWatchQueryOptions({ client, query, options }); const { fetchPolicy, variables } = watchQueryOptions; const { queryKey = [] } = options; + const canonicalVariables = canonicalStringify(variables); + + // This state value let's us maintain the variables used for the cache key + // when `skipToken` is used to skip a query after its been executed. + // Since options are provided when using `skipToken`, `variables` disappear, + // which means a cache key without a variables value is used to create a new + // `ObservableQuery` instance. This was particularly problematic when + // `refetchQueries` was used because it meant refetching against an + // `ObservableQuery` instance that had no variables. + let [cacheKeyVariables, setCacheKeyVariables] = + React.useState(canonicalVariables); + + if (options !== skipToken && cacheKeyVariables !== canonicalVariables) { + setCacheKeyVariables((cacheKeyVariables = canonicalVariables)); + } // This ref tracks the first time query execution is enabled to determine // whether to return a query ref or `undefined`. When initialized @@ -463,7 +479,7 @@ function useBackgroundQuery_< const cacheKey: CacheKey = [ query, - canonicalStringify(variables), + cacheKeyVariables, ...([] as any[]).concat(queryKey), ]; From e58febcc808154d7571436f2479d92cd0f828524 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 11:27:52 -0600 Subject: [PATCH 03/33] Add test to ensure changing variables after skipToken suspends --- .../__tests__/useBackgroundQuery.test.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 5fbadef37db..0ed827ee0f4 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -2013,6 +2013,92 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer await expect(renderStream).not.toRerender(); }); +test("suspends and fetches when changing variables when no longer using skipToken", async () => { + const { query, mocks } = setupVariablesCase(); + const renderStream = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(renderStream); + + function App({ id }: { id: string | undefined }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ); + + return ( + <> + }> + {queryRef && } + + + ); + } + + using _disabledAct = disableActEnvironment(); + const { rerender } = await renderStream.render(, { + wrapper: createMockWrapper({ mocks }), + }); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await renderStream.takeRender(); + + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await renderStream.takeRender(); + + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender(); +}); + it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { const { query, mocks } = setupSimpleCase(); const user = userEvent.setup(); From 498c4c88759b72585b85276a41fbb0a50a86541e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 11:32:48 -0600 Subject: [PATCH 04/33] Add test to ensure cached value is honored --- .../__tests__/useBackgroundQuery.test.tsx | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 0ed827ee0f4..bc9d3c2a81a 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -2099,6 +2099,97 @@ test("suspends and fetches when changing variables when no longer using skipToke await expect(renderStream).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 renderStream = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(renderStream); + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + data: { + character: { __typename: "Character", id: "2", name: "Cached Widow" }, + }, + variables: { id: "2" }, + }); + + function App({ id }: { id: string | undefined }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ); + + return ( + <> + }> + {queryRef && } + + + ); + } + + using _disabledAct = disableActEnvironment(); + const { rerender } = await renderStream.render(, { + wrapper: createMockWrapper({ cache, mocks }), + }); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await renderStream.takeRender(); + + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "2", name: "Cached Widow" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender(); +}); + it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { const { query, mocks } = setupSimpleCase(); const user = userEvent.setup(); From 1994e640d6a812cb19de9d398234f94aa8aeace2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 11:34:32 -0600 Subject: [PATCH 05/33] Swap order of tests --- .../__tests__/useBackgroundQuery.test.tsx | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index bc9d3c2a81a..15d470e5d20 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -1910,6 +1910,65 @@ it("renders skip result, does not suspend, and maintains `data` when `skip` beco await expect(renderStream).not.toRerender({ timeout: 50 }); }); +it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const renderStream = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(renderStream); + + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + using _disabledAct = disableActEnvironment(); + await renderStream.render(, { wrapper: createMockWrapper({ mocks }) }); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await renderStream.takeRender(); + + expect(snapshot.result).toStrictEqualTyped({ + data: { greeting: "Hello" }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await user.click(screen.getByText("Toggle skip")); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { greeting: "Hello" }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender({ timeout: 50 }); +}); + // 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(); @@ -2190,65 +2249,6 @@ test("does not suspend for data in the cache when changing variables when no lon await expect(renderStream).not.toRerender(); }); -it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { - const { query, mocks } = setupSimpleCase(); - const user = userEvent.setup(); - const renderStream = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - function App() { - useTrackRenders(); - const [skip, setSkip] = React.useState(false); - const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - - return ( - <> - - }> - {queryRef && } - - - ); - } - - using _disabledAct = disableActEnvironment(); - await renderStream.render(, { wrapper: createMockWrapper({ mocks }) }); - - { - const { renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } - - { - const { snapshot } = await renderStream.takeRender(); - - expect(snapshot.result).toStrictEqualTyped({ - data: { greeting: "Hello" }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await user.click(screen.getByText("Toggle skip")); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { greeting: "Hello" }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender({ timeout: 50 }); -}); - it("does not make network requests when `skip` is `true`", async () => { const { query, mocks } = setupSimpleCase(); const user = userEvent.setup(); From f7b0744d3c2cbd56ae76f3ec7c37d12580b41daf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:13:34 -0600 Subject: [PATCH 06/33] Add tests for skipToken with useSuspenseQuery --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 334 +++++++++++++++++- 1 file changed, 333 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 7122e12171a..bac70419269 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -12,7 +12,7 @@ import { GraphQLError } from "graphql"; import React, { Fragment, StrictMode, Suspense, useTransition } from "react"; import type { FallbackProps } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary"; -import { Observable, of } from "rxjs"; +import { delay, Observable, of } from "rxjs"; import type { ApolloCache, @@ -46,14 +46,18 @@ import { MockLink, MockSubscriptionLink } from "@apollo/client/testing"; import type { PaginatedCaseData, PaginatedCaseVariables, + VariablesCaseData as VariablesScenarioData, + VariablesCaseVariables as VariablesScenarioVariables, } from "@apollo/client/testing/internal"; import { actAsync, createClientWrapper, + createMockWrapper, markAsStreaming, renderAsync, renderHookAsync, setupPaginatedCase, + setupVariablesCase, spyOnConsole, } from "@apollo/client/testing/internal"; import { MockedProvider } from "@apollo/client/testing/react"; @@ -5828,6 +5832,334 @@ describe("useSuspenseQuery", () => { ]); }); + // 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 { takeRender, render, replaceSnapshot } = createRenderStream< + useSuspenseQuery.Result< + VariablesScenarioData, + VariablesScenarioVariables, + "empty" | "streaming" | "complete" + > + >({ skipNonTrackingRenders: true }); + + 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(), + }); + + function SuspenseFallback() { + useTrackRenders(); + + return null; + } + + function UseSuspenseQuery({ id }: { id: string | undefined }) { + useTrackRenders(); + replaceSnapshot( + useSuspenseQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ) + ); + + return null; + } + + function App({ id }: { id: string | undefined }) { + return ( + }> + + + ); + } + + using _disabledAct = disableActEnvironment(); + + const { rerender } = await render(, { + wrapper: createClientWrapper(client), + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await takeRender(); + + expect(snapshot).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + 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(); + const { takeRender, render, replaceSnapshot } = + createRenderStream< + useSuspenseQuery.Result< + VariablesScenarioData, + VariablesScenarioVariables, + "empty" | "streaming" | "complete" + > + >(); + + function SuspenseFallback() { + useTrackRenders(); + + return null; + } + + function UseSuspenseQuery({ id }: { id: string | undefined }) { + useTrackRenders(); + replaceSnapshot( + useSuspenseQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ) + ); + + return null; + } + + function App({ id }: { id: string | undefined }) { + return ( + }> + + + ); + } + + using _disabledAct = disableActEnvironment(); + const { rerender } = await render(, { + wrapper: createMockWrapper({ mocks }), + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await takeRender(); + + expect(snapshot).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + 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 rerender(); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + 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 { takeRender, render, replaceSnapshot } = createRenderStream< + useSuspenseQuery.Result< + VariablesScenarioData, + VariablesScenarioVariables, + "empty" | "streaming" | "complete" + > + >({ skipNonTrackingRenders: true }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + client.writeQuery({ + query, + data: { + character: { __typename: "Character", id: "2", name: "Cached Widow" }, + }, + variables: { id: "2" }, + }); + + function SuspenseFallback() { + useTrackRenders(); + + return null; + } + + function UseSuspenseQuery({ id }: { id: string | undefined }) { + useTrackRenders(); + replaceSnapshot( + useSuspenseQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ) + ); + + return null; + } + + function App({ id }: { id: string | undefined }) { + return ( + }> + + + ); + } + + using _disabledAct = disableActEnvironment(); + const { rerender } = await render(, { + wrapper: createClientWrapper(client), + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await takeRender(); + + expect(snapshot).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + expect(snapshot).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await rerender(); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + expect(snapshot).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "2", name: "Cached Widow" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(takeRender).not.toRerender(); + }); + it("does not make network requests when `skip` is `true`", async () => { const { query, mocks } = useVariablesQueryCase(); From dfd7459a0387eee07bf585199ee5ecaf8350c4ed Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:24:14 -0600 Subject: [PATCH 07/33] Add helper for useSuspenseQuery --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index bac70419269..de4e0edf316 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -1,4 +1,4 @@ -import type { RenderHookOptions } from "@testing-library/react"; +import type { RenderHookOptions, RenderOptions } from "@testing-library/react"; import { act, renderHook, screen, waitFor } from "@testing-library/react"; import { createRenderStream, @@ -18,6 +18,7 @@ import type { ApolloCache, DataValue, DocumentNode, + ErrorLike, ErrorPolicy, ObservableQuery, OperationVariables, @@ -67,7 +68,7 @@ import { offsetLimitPagination, } from "@apollo/client/utilities"; import { compact, getMainDefinition } from "@apollo/client/utilities/internal"; -import { InvariantError } from "@apollo/client/utilities/invariant"; +import { invariant, InvariantError } from "@apollo/client/utilities/invariant"; import type { RefetchWritePolicy, @@ -100,6 +101,71 @@ interface SimpleQueryData { greeting: string; } +async function renderUseSuspenseQueryHook< + TData, + TVariables extends OperationVariables, + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => useSuspenseQuery.Result, + options: Pick & { initialProps?: Props } +) { + function UseSuspenseQuery({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useSuspenseQuery" }); + replaceSnapshot(renderHook(props as any)); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + + return null; + } + + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { + return ( + }> + replaceSnapshot({ error })} + > + + + + ); + } + + const { render, takeRender, replaceSnapshot, getCurrentRender } = + createRenderStream< + useSuspenseQuery.Result | { error: ErrorLike } + >({ skipNonTrackingRenders: true }); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + function getCurrentSnapshot() { + const { snapshot } = getCurrentRender(); + + invariant("data" in snapshot, "Snapshot is not a hook snapshot"); + + return snapshot; + } + + return { getCurrentSnapshot, takeRender, rerender }; +} + +/** @deprecated use `renderUseSuspenseQueryHook` which uses renderSnapshotStream */ async function renderSuspenseHook( render: (initialProps: Props) => Result, options: RenderSuspenseHookOptions = {} From 25803f7937da65aadf42b3d9110c94b958140e69 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:31:40 -0600 Subject: [PATCH 08/33] Use helper in new tests --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 157 +++++------------- 1 file changed, 40 insertions(+), 117 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index de4e0edf316..57409071e4d 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -5901,13 +5901,6 @@ describe("useSuspenseQuery", () => { // 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 { takeRender, render, replaceSnapshot } = createRenderStream< - useSuspenseQuery.Result< - VariablesScenarioData, - VariablesScenarioVariables, - "empty" | "streaming" | "complete" - > - >({ skipNonTrackingRenders: true }); const client = new ApolloClient({ link: new ApolloLink((operation) => { @@ -5933,42 +5926,23 @@ describe("useSuspenseQuery", () => { cache: new InMemoryCache(), }); - function SuspenseFallback() { - useTrackRenders(); - - return null; - } - - function UseSuspenseQuery({ id }: { id: string | undefined }) { - useTrackRenders(); - replaceSnapshot( + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderUseSuspenseQueryHook( + ({ id }) => useSuspenseQuery( query, id === undefined ? skipToken : { variables: { id } } - ) - ); - - return null; - } - - function App({ id }: { id: string | undefined }) { - return ( - }> - - - ); - } - - using _disabledAct = disableActEnvironment(); - - const { rerender } = await render(, { - wrapper: createClientWrapper(client), - }); + ), + { + initialProps: { id: "1" as string | undefined }, + wrapper: createClientWrapper(client), + } + ); { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } { @@ -5984,12 +5958,12 @@ describe("useSuspenseQuery", () => { }); } - await rerender(); + await rerender({ id: undefined }); { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); expect(snapshot).toStrictEqualTyped({ data: { character: { __typename: "Character", id: "1", name: "Spider-Man" }, @@ -6017,50 +5991,24 @@ describe("useSuspenseQuery", () => { test("suspends and fetches when changing variables when no longer using skipToken", async () => { const { query, mocks } = setupVariablesCase(); - const { takeRender, render, replaceSnapshot } = - createRenderStream< - useSuspenseQuery.Result< - VariablesScenarioData, - VariablesScenarioVariables, - "empty" | "streaming" | "complete" - > - >(); - - function SuspenseFallback() { - useTrackRenders(); - - return null; - } - function UseSuspenseQuery({ id }: { id: string | undefined }) { - useTrackRenders(); - replaceSnapshot( + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderUseSuspenseQueryHook( + ({ id }) => useSuspenseQuery( query, id === undefined ? skipToken : { variables: { id } } - ) - ); - - return null; - } - - function App({ id }: { id: string | undefined }) { - return ( - }> - - - ); - } - - using _disabledAct = disableActEnvironment(); - const { rerender } = await render(, { - wrapper: createMockWrapper({ mocks }), - }); + ), + { + initialProps: { id: "1" as string | undefined }, + wrapper: createMockWrapper({ mocks }), + } + ); { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } { @@ -6076,12 +6024,12 @@ describe("useSuspenseQuery", () => { }); } - await rerender(); + await rerender({ id: undefined }); { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); expect(snapshot).toStrictEqualTyped({ data: { character: { __typename: "Character", id: "1", name: "Spider-Man" }, @@ -6094,12 +6042,12 @@ describe("useSuspenseQuery", () => { await expect(takeRender).not.toRerender(); - await rerender(); + await rerender({ id: "2" }); { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } { @@ -6120,13 +6068,6 @@ describe("useSuspenseQuery", () => { test("does not suspend for data in the cache when changing variables when no longer using skipToken", async () => { const { query, mocks } = setupVariablesCase(); - const { takeRender, render, replaceSnapshot } = createRenderStream< - useSuspenseQuery.Result< - VariablesScenarioData, - VariablesScenarioVariables, - "empty" | "streaming" | "complete" - > - >({ skipNonTrackingRenders: true }); const client = new ApolloClient({ link: new MockLink(mocks), @@ -6141,41 +6082,23 @@ describe("useSuspenseQuery", () => { variables: { id: "2" }, }); - function SuspenseFallback() { - useTrackRenders(); - - return null; - } - - function UseSuspenseQuery({ id }: { id: string | undefined }) { - useTrackRenders(); - replaceSnapshot( + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderUseSuspenseQueryHook( + ({ id }) => useSuspenseQuery( query, id === undefined ? skipToken : { variables: { id } } - ) - ); - - return null; - } - - function App({ id }: { id: string | undefined }) { - return ( - }> - - - ); - } - - using _disabledAct = disableActEnvironment(); - const { rerender } = await render(, { - wrapper: createClientWrapper(client), - }); + ), + { + initialProps: { id: "1" as string | undefined }, + wrapper: createClientWrapper(client), + } + ); { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } { @@ -6191,12 +6114,12 @@ describe("useSuspenseQuery", () => { }); } - await rerender(); + await rerender({ id: undefined }); { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); expect(snapshot).toStrictEqualTyped({ data: { character: { __typename: "Character", id: "1", name: "Spider-Man" }, @@ -6207,12 +6130,12 @@ describe("useSuspenseQuery", () => { }); } - await rerender(); + await rerender({ id: "2" }); { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([UseSuspenseQuery]); + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); expect(snapshot).toStrictEqualTyped({ data: { character: { __typename: "Character", id: "2", name: "Cached Widow" }, From dfaf2a818b1bbca81a1eb4c3148074d3abe49c5f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:31:54 -0600 Subject: [PATCH 09/33] Add fix to useSuspenseQuery --- src/react/hooks/useSuspenseQuery.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index b8ff4434a72..5b061628930 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -377,10 +377,25 @@ function useSuspenseQuery_< }); const { fetchPolicy, variables } = watchQueryOptions; const { queryKey = [] } = options; + const canonicalVariables = canonicalStringify(variables); + + // This state value let's us maintain the variables used for the cache key + // when `skipToken` is used to skip a query after its been executed. + // Since options are provided when using `skipToken`, `variables` disappear, + // which means a cache key without a variables value is used to create a new + // `ObservableQuery` instance. This was particularly problematic when + // `refetchQueries` was used because it meant refetching against an + // `ObservableQuery` instance that had no variables. + let [cacheKeyVariables, setCacheKeyVariables] = + React.useState(canonicalVariables); + + if (options !== skipToken && cacheKeyVariables !== canonicalVariables) { + setCacheKeyVariables((cacheKeyVariables = canonicalVariables)); + } const cacheKey: CacheKey = [ query, - canonicalStringify(variables), + cacheKeyVariables, ...([] as any[]).concat(queryKey), ]; From 058b470296941c2fe210ffd10efd8621a8d80e60 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:33:33 -0600 Subject: [PATCH 10/33] Remove unused imports --- src/react/hooks/__tests__/useSuspenseQuery.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 57409071e4d..96948221a1d 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -47,8 +47,6 @@ import { MockLink, MockSubscriptionLink } from "@apollo/client/testing"; import type { PaginatedCaseData, PaginatedCaseVariables, - VariablesCaseData as VariablesScenarioData, - VariablesCaseVariables as VariablesScenarioVariables, } from "@apollo/client/testing/internal"; import { actAsync, From 642648eba0224268e17bcd8f91cc5e0499864da1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:51:07 -0600 Subject: [PATCH 11/33] Move tests to own folder --- config/jest.config.ts | 10 +- .../hooks/__tests__/useSuspenseQuery.test.tsx | 325 +----------------- .../useSuspenseQuery/skipToken.test.tsx | 269 +++++++++++++++ .../__tests__/useSuspenseQuery/utils.tsx | 73 ++++ 4 files changed, 352 insertions(+), 325 deletions(-) create mode 100644 src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx create mode 100644 src/react/hooks/__tests__/useSuspenseQuery/utils.tsx diff --git a/config/jest.config.ts b/config/jest.config.ts index 9e8be6190dc..accfbd82f84 100644 --- a/config/jest.config.ts +++ b/config/jest.config.ts @@ -39,8 +39,6 @@ const ignoreDTSFiles = ".d.ts$"; const ignoreTSFiles = ".ts$"; const ignoreTSXFiles = ".tsx$"; -const react19TestFileIgnoreList = [ignoreDTSFiles, ignoreTSFiles]; - const react17TestFileIgnoreList = [ ignoreDTSFiles, ignoreTSFiles, @@ -49,6 +47,7 @@ const react17TestFileIgnoreList = [ "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__/useLoadableQuery.test.tsx", "src/react/hooks/__tests__/useQueryRefHandlers.test.tsx", @@ -81,7 +80,11 @@ const tsRxJSMinConfig = { const standardReact19Config = { ...defaults, displayName: "ReactDOM 19", - testPathIgnorePatterns: react19TestFileIgnoreList, + testPathIgnorePatterns: [ + ignoreDTSFiles, + ignoreTSFiles, + "src/react/hooks/__tests__/useSuspenseQuery/utils.tsx", + ], }; const standardReact18Config = { @@ -91,6 +94,7 @@ const standardReact18Config = { ignoreDTSFiles, ignoreTSFiles, "src/react/ssr/__tests__/prerenderStatic.test.tsx", + "src/react/hooks/__tests__/useSuspenseQuery/utils.tsx", ], moduleNameMapper: { ...defaults.moduleNameMapper, diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 96948221a1d..7122e12171a 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -1,4 +1,4 @@ -import type { RenderHookOptions, RenderOptions } from "@testing-library/react"; +import type { RenderHookOptions } from "@testing-library/react"; import { act, renderHook, screen, waitFor } from "@testing-library/react"; import { createRenderStream, @@ -12,13 +12,12 @@ import { GraphQLError } from "graphql"; import React, { Fragment, StrictMode, Suspense, useTransition } from "react"; import type { FallbackProps } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary"; -import { delay, Observable, of } from "rxjs"; +import { Observable, of } from "rxjs"; import type { ApolloCache, DataValue, DocumentNode, - ErrorLike, ErrorPolicy, ObservableQuery, OperationVariables, @@ -51,12 +50,10 @@ import type { import { actAsync, createClientWrapper, - createMockWrapper, markAsStreaming, renderAsync, renderHookAsync, setupPaginatedCase, - setupVariablesCase, spyOnConsole, } from "@apollo/client/testing/internal"; import { MockedProvider } from "@apollo/client/testing/react"; @@ -66,7 +63,7 @@ import { offsetLimitPagination, } from "@apollo/client/utilities"; import { compact, getMainDefinition } from "@apollo/client/utilities/internal"; -import { invariant, InvariantError } from "@apollo/client/utilities/invariant"; +import { InvariantError } from "@apollo/client/utilities/invariant"; import type { RefetchWritePolicy, @@ -99,71 +96,6 @@ interface SimpleQueryData { greeting: string; } -async function renderUseSuspenseQueryHook< - TData, - TVariables extends OperationVariables, - Props = never, ->( - renderHook: ( - props: Props extends never ? undefined : Props - ) => useSuspenseQuery.Result, - options: Pick & { initialProps?: Props } -) { - function UseSuspenseQuery({ props }: { props: Props | undefined }) { - useTrackRenders({ name: "useSuspenseQuery" }); - replaceSnapshot(renderHook(props as any)); - - return null; - } - - function SuspenseFallback() { - useTrackRenders({ name: "SuspenseFallback" }); - - return null; - } - - function ErrorFallback() { - useTrackRenders({ name: "ErrorBoundary" }); - - return null; - } - - function App({ props }: { props: Props | undefined }) { - return ( - }> - replaceSnapshot({ error })} - > - - - - ); - } - - const { render, takeRender, replaceSnapshot, getCurrentRender } = - createRenderStream< - useSuspenseQuery.Result | { error: ErrorLike } - >({ skipNonTrackingRenders: true }); - - const utils = await render(, options); - - function rerender(props: Props) { - return utils.rerender(); - } - - function getCurrentSnapshot() { - const { snapshot } = getCurrentRender(); - - invariant("data" in snapshot, "Snapshot is not a hook snapshot"); - - return snapshot; - } - - return { getCurrentSnapshot, takeRender, rerender }; -} - -/** @deprecated use `renderUseSuspenseQueryHook` which uses renderSnapshotStream */ async function renderSuspenseHook( render: (initialProps: Props) => Result, options: RenderSuspenseHookOptions = {} @@ -5896,257 +5828,6 @@ describe("useSuspenseQuery", () => { ]); }); - // 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 { takeRender, rerender } = await renderUseSuspenseQueryHook( - ({ id }) => - useSuspenseQuery( - query, - id === undefined ? skipToken : { variables: { id } } - ), - { - initialProps: { id: "1" as string | undefined }, - wrapper: createClientWrapper(client), - } - ); - - { - const { renderedComponents } = await takeRender(); - - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); - } - - { - const { snapshot } = await takeRender(); - - 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(["useSuspenseQuery"]); - 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(); - - using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderUseSuspenseQueryHook( - ({ id }) => - useSuspenseQuery( - query, - id === undefined ? skipToken : { variables: { id } } - ), - { - initialProps: { id: "1" as string | undefined }, - wrapper: createMockWrapper({ mocks }), - } - ); - - { - const { renderedComponents } = await takeRender(); - - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); - } - - { - const { snapshot } = await takeRender(); - - 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(["useSuspenseQuery"]); - 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 rerender({ id: "2" }); - - { - const { renderedComponents } = await takeRender(); - - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); - } - - { - 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 client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); - - client.writeQuery({ - query, - data: { - character: { __typename: "Character", id: "2", name: "Cached Widow" }, - }, - variables: { id: "2" }, - }); - - using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderUseSuspenseQueryHook( - ({ id }) => - useSuspenseQuery( - query, - id === undefined ? skipToken : { variables: { id } } - ), - { - initialProps: { id: "1" as string | undefined }, - wrapper: createClientWrapper(client), - } - ); - - { - const { renderedComponents } = await takeRender(); - - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); - } - - { - const { snapshot } = await takeRender(); - - 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(["useSuspenseQuery"]); - 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(["useSuspenseQuery"]); - expect(snapshot).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "2", name: "Cached Widow" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(takeRender).not.toRerender(); - }); - it("does not make network requests when `skip` is `true`", async () => { const { query, mocks } = useVariablesQueryCase(); diff --git a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx new file mode 100644 index 00000000000..4402d67bab9 --- /dev/null +++ b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx @@ -0,0 +1,269 @@ +import { disableActEnvironment } from "@testing-library/react-render-stream"; +import { delay, of } from "rxjs"; + +import { + ApolloClient, + ApolloLink, + InMemoryCache, + NetworkStatus, +} from "@apollo/client"; +import { skipToken, useSuspenseQuery } from "@apollo/client/react"; +import { MockLink } from "@apollo/client/testing"; +import { + createClientWrapper, + createMockWrapper, + setupVariablesCase, +} from "@apollo/client/testing/internal"; + +import { renderSuspenseHook } from "./utils.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 { takeRender, rerender } = await renderSuspenseHook( + ({ id }) => + useSuspenseQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ), + { + initialProps: { id: "1" as string | undefined }, + wrapper: createClientWrapper(client), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot } = await takeRender(); + + 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(["useSuspenseQuery"]); + 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(); + + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderSuspenseHook( + ({ id }) => + useSuspenseQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ), + { + initialProps: { id: "1" as string | undefined }, + wrapper: createMockWrapper({ mocks }), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot } = await takeRender(); + + 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(["useSuspenseQuery"]); + 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 rerender({ id: "2" }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + 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 client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + client.writeQuery({ + query, + data: { + character: { __typename: "Character", id: "2", name: "Cached Widow" }, + }, + variables: { id: "2" }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderSuspenseHook( + ({ id }) => + useSuspenseQuery( + query, + id === undefined ? skipToken : { variables: { id } } + ), + { + initialProps: { id: "1" as string | undefined }, + wrapper: createClientWrapper(client), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot } = await takeRender(); + + 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(["useSuspenseQuery"]); + 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(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + character: { __typename: "Character", id: "2", name: "Cached Widow" }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(takeRender).not.toRerender(); +}); diff --git a/src/react/hooks/__tests__/useSuspenseQuery/utils.tsx b/src/react/hooks/__tests__/useSuspenseQuery/utils.tsx new file mode 100644 index 00000000000..a28cf4b1136 --- /dev/null +++ b/src/react/hooks/__tests__/useSuspenseQuery/utils.tsx @@ -0,0 +1,73 @@ +import type { RenderOptions } from "@testing-library/react"; +import { + createRenderStream, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { ErrorLike, OperationVariables } from "@apollo/client"; +import type { useSuspenseQuery } from "@apollo/client/react"; +import { invariant } from "@apollo/client/utilities/invariant"; + +export async function renderSuspenseHook< + TData, + TVariables extends OperationVariables, + Props = never, +>( + renderHook: (props: any) => any, + options: Pick & { initialProps?: Props } +) { + function UseSuspenseQuery({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useSuspenseQuery" }); + replaceSnapshot(renderHook(props as any)); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + + return null; + } + + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { + return ( + }> + replaceSnapshot({ error })} + > + + + + ); + } + + const { render, takeRender, replaceSnapshot, getCurrentRender } = + createRenderStream< + useSuspenseQuery.Result | { error: ErrorLike } + >({ skipNonTrackingRenders: true }); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + function getCurrentSnapshot() { + const { snapshot } = getCurrentRender(); + + invariant("data" in snapshot, "Snapshot is not a hook snapshot"); + + return snapshot; + } + + return { getCurrentSnapshot, rerender, takeRender }; +} From a7fe5751a20739be39f85fe273c196c87b2639a2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:53:21 -0600 Subject: [PATCH 12/33] Deprecate old renderSuspenseHook helper --- src/react/hooks/__tests__/useSuspenseQuery.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 7122e12171a..dac8507c428 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -96,6 +96,11 @@ interface SimpleQueryData { greeting: string; } +/** + * @deprecated + * Use `renderSuspenseHook` aliased to `renderUseSuspenseQueryHook` from utils + * which uses render streams instead of render counting. + */ async function renderSuspenseHook( render: (initialProps: Props) => Result, options: RenderSuspenseHookOptions = {} From 9e151ccd95e608f0b4de0ca4723c020234bb5fd6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 12:58:08 -0600 Subject: [PATCH 13/33] Update tests to use render helper --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 879 +++++++++--------- 1 file changed, 452 insertions(+), 427 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index dac8507c428..193b4e10251 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -8,6 +8,7 @@ import { import { userEvent } from "@testing-library/user-event"; import { equal } from "@wry/equality"; import { expectTypeOf } from "expect-type"; +import type { GraphQLFormattedError } from "graphql"; import { GraphQLError } from "graphql"; import React, { Fragment, StrictMode, Suspense, useTransition } from "react"; import type { FallbackProps } from "react-error-boundary"; @@ -50,6 +51,7 @@ import type { import { actAsync, createClientWrapper, + createMockWrapper, markAsStreaming, renderAsync, renderHookAsync, @@ -70,6 +72,8 @@ import type { WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions.js"; +import { renderSuspenseHook as renderUseSuspenseQueryHook } from "./useSuspenseQuery/utils.js"; + const IS_REACT_19 = React.version.startsWith("19"); type RenderSuspenseHookOptions = Omit< @@ -1090,47 +1094,51 @@ describe("useSuspenseQuery", () => { it("suspends when changing variables", async () => { const { query, mocks } = useVariablesQueryCase(); - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { variables: { id } }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it("suspends and fetches data from new client when changing clients", async () => { @@ -1569,135 +1577,137 @@ describe("useSuspenseQuery", () => { link: new MockLink(mocks), }); - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { variables: { id } }), - { client, initialProps: { id: "1" } } + { wrapper: createClientWrapper(client), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - act(() => { - client.writeQuery({ - query, - variables: { id: "2" }, - data: { character: { id: "2", name: "Cached hero" } }, - }); + client.writeQuery({ + query, + variables: { id: "2" }, + data: { character: { id: "2", name: "Cached hero" } }, }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { character: { id: "2", name: "Cached hero" } }, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(5 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: { character: { id: "2", name: "Cached hero" } }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it("uses cached result and does not suspend when switching back to already used variables while using `cache-first` fetch policy", async () => { const { query, mocks } = useVariablesQueryCase(); - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "cache-first", variables: { id }, }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "1" }); + await rerender({ id: "1" }); - expect(result.current).toStrictEqualTyped({ - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); + { + const { snapshot, renderedComponents } = await takeRender(); - expect(renders.count).toBe(5 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, - }, - ]); + }); + } + + await expect(takeRender).not.toRerender(); }); it("uses cached result with network request and does not suspend when switching back to already used variables while using `cache-and-network` fetch policy", async () => { @@ -1749,81 +1759,81 @@ describe("useSuspenseQuery", () => { }, ]; - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "cache-and-network", variables: { id }, }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); + { + const { renderedComponents } = await takeRender(); - await rerenderAsync({ id: "1" }); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } - expect(result.current).toStrictEqualTyped({ - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.loading, - error: undefined, - }); + { + const { snapshot, renderedComponents } = await takeRender(); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - ...mocks[2].result, + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(6 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { + await rerender({ id: "1" }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.loading, error: undefined, - }, - { + }); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[2].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, - }, - ]); + }); + } + + await expect(takeRender).not.toRerender(); }); it("refetches and suspends when switching back to already used variables while using `network-only` fetch policy", async () => { @@ -1875,68 +1885,75 @@ describe("useSuspenseQuery", () => { }, ]; - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "network-only", variables: { id }, }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, - networkStatus: NetworkStatus.ready, dataState: "complete", + networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "1" }); + await rerender({ id: "1" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[2].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(6 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(3); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[2].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it("refetches and suspends when switching back to already used variables while using `no-cache` fetch policy", async () => { @@ -1988,68 +2005,75 @@ describe("useSuspenseQuery", () => { }, ]; - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "no-cache", variables: { id }, }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "1" }); + await rerender({ id: "1" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[2].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(6 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(3); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[2].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it("responds to cache updates after changing back to already fetched variables", async () => { @@ -2060,87 +2084,83 @@ describe("useSuspenseQuery", () => { link: new MockLink(mocks), }); - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { variables: { id } }), - { client, initialProps: { id: "1" } } + { wrapper: createClientWrapper(client), initialProps: { id: "1" } } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "1" }); + await rerender({ id: "1" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - act(() => { - client.writeQuery({ - query, - variables: { id: "1" }, - data: { character: { id: "1", name: "Cached hero" } }, - }); + client.writeQuery({ + query, + variables: { id: "1" }, + data: { character: { id: "1", name: "Cached hero" } }, }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { character: { id: "1", name: "Cached hero" } }, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(6 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: { character: { id: "1", name: "Cached hero" } }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it('does not suspend when data is in the cache and using a "cache-first" fetch policy', async () => { @@ -2362,66 +2382,65 @@ describe("useSuspenseQuery", () => { variables: { id: "1" }, }); - const { result, renders, rerenderAsync } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(fullQuery, { fetchPolicy: "cache-first", returnPartialData: true, variables: { id }, }), - { cache, mocks, initialProps: { id: "1" } } + { + wrapper: createMockWrapper({ cache, mocks }), + initialProps: { id: "1" }, + } ); - expect(renders.suspenseCount).toBe(0); - expect(result.current).toStrictEqualTyped({ - data: { character: { id: "1" } }, - dataState: "partial", - networkStatus: NetworkStatus.loading, - error: undefined, - }); + { + const { snapshot, renderedComponents } = await takeRender(); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { character: { id: "1" } }, + dataState: "partial", + networkStatus: NetworkStatus.loading, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + { + const { snapshot, renderedComponents } = await takeRender(); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - ...mocks[1].result, + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: { character: { id: "1" } }, - dataState: "partial", - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { + await rerender({ id: "2" }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, - }, - ]); + }); + } + + await expect(takeRender).not.toRerender(); }); it('suspends when data is in the cache and using a "network-only" fetch policy', async () => { @@ -2852,66 +2871,65 @@ describe("useSuspenseQuery", () => { variables: { id: "1" }, }); - const { result, renders, rerenderAsync } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(fullQuery, { fetchPolicy: "cache-and-network", returnPartialData: true, variables: { id }, }), - { cache, mocks, initialProps: { id: "1" } } + { + wrapper: createMockWrapper({ cache, mocks }), + initialProps: { id: "1" }, + } ); - expect(renders.suspenseCount).toBe(0); - expect(result.current).toStrictEqualTyped({ - data: { character: { id: "1" } }, - dataState: "partial", - networkStatus: NetworkStatus.loading, - error: undefined, - }); + { + const { snapshot, renderedComponents } = await takeRender(); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { character: { id: "1" } }, + dataState: "partial", + networkStatus: NetworkStatus.loading, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + { + const { snapshot, renderedComponents } = await takeRender(); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - ...mocks[1].result, + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: { character: { id: "1" } }, - dataState: "partial", - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { + await rerender({ id: "2" }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, - }, - ]); + }); + } + + await expect(takeRender).not.toRerender(); }); it.each([ @@ -3070,53 +3088,51 @@ describe("useSuspenseQuery", () => { async (fetchPolicy) => { const { query, mocks } = useVariablesQueryCase(); - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { fetchPolicy, variables: { id } }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); - expect(renders.suspenseCount).toBe(1); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[0].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ ...mocks[1].result, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - // Renders: - // 1. Initiate fetch and suspend - // 2. Unsuspend and return results from initial fetch - // 3. Change variables and suspend - // 5. Unsuspend and return results from refetch - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - ...mocks[0].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); } ); @@ -3530,17 +3546,30 @@ describe("useSuspenseQuery", () => { }, }); - const { result, rerenderAsync, renders } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderUseSuspenseQueryHook( ({ source }) => useSuspenseQuery(query, { fetchPolicy: "network-only", variables: { source, localOnlyVar: true }, }), - { client, initialProps: { source: "local" } } + { + wrapper: createClientWrapper(client), + initialProps: { source: "local" }, + } ); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { vars: { source: "local", globalOnlyVar: true, localOnlyVar: true }, }, @@ -3548,12 +3577,21 @@ describe("useSuspenseQuery", () => { networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - await rerenderAsync({ source: "rerender" }); + await rerender({ source: "rerender" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { vars: { source: "rerender", globalOnlyVar: true, localOnlyVar: true }, }, @@ -3561,26 +3599,9 @@ describe("useSuspenseQuery", () => { networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.frames).toStrictEqualTyped([ - { - data: { - vars: { source: "local", globalOnlyVar: true, localOnlyVar: true }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: { - vars: { source: "rerender", globalOnlyVar: true, localOnlyVar: true }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it("can unset a globally defined variable", async () => { @@ -4354,7 +4375,9 @@ describe("useSuspenseQuery", () => { } `; - const graphQLErrors = [new GraphQLError("Could not fetch user 1")]; + const graphQLErrors: GraphQLFormattedError[] = [ + { message: "Could not fetch user 1" }, + ]; const mocks = [ { @@ -4373,52 +4396,54 @@ describe("useSuspenseQuery", () => { }, ]; - const { result, renders, rerenderAsync } = await renderSuspenseHook( + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderUseSuspenseQueryHook( ({ id }) => useSuspenseQuery(query, { errorPolicy: "all", variables: { id } }), - { mocks, initialProps: { id: "1" } } + { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); const expectedError = new CombinedGraphQLErrors({ errors: graphQLErrors }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ data: undefined, dataState: "empty", networkStatus: NetworkStatus.error, error: expectedError, }); - }); + } - await rerenderAsync({ id: "2" }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ data: mocks[1].result.data, dataState: "complete", networkStatus: NetworkStatus.ready, error: undefined, }); - }); + } - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - data: undefined, - dataState: "empty", - networkStatus: NetworkStatus.error, - error: expectedError, - }, - { - data: mocks[1].result.data, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + await expect(takeRender).not.toRerender(); }); it("re-suspends when calling `refetch`", async () => { From 04a757ccdd3c6841f6421194040243dd71a20589 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 13:34:42 -0600 Subject: [PATCH 14/33] Rename file to testUtils --- src/react/hooks/__tests__/useSuspenseQuery.test.tsx | 2 +- src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx | 2 +- .../__tests__/useSuspenseQuery/{utils.tsx => testUtils.tsx} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/react/hooks/__tests__/useSuspenseQuery/{utils.tsx => testUtils.tsx} (100%) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 193b4e10251..6ce978c67a5 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -72,7 +72,7 @@ import type { WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions.js"; -import { renderSuspenseHook as renderUseSuspenseQueryHook } from "./useSuspenseQuery/utils.js"; +import { renderSuspenseHook as renderUseSuspenseQueryHook } from "./useSuspenseQuery/testUtils.js"; const IS_REACT_19 = React.version.startsWith("19"); diff --git a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx index 4402d67bab9..d24596b0103 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx @@ -15,7 +15,7 @@ import { setupVariablesCase, } from "@apollo/client/testing/internal"; -import { renderSuspenseHook } from "./utils.js"; +import { renderSuspenseHook } 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 () => { diff --git a/src/react/hooks/__tests__/useSuspenseQuery/utils.tsx b/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx similarity index 100% rename from src/react/hooks/__tests__/useSuspenseQuery/utils.tsx rename to src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx From ae8b1324fb03e547762cbb2191bae217913ddd8a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 13:35:39 -0600 Subject: [PATCH 15/33] Add a testUtils file for useBackgroundQuery --- config/jest.config.ts | 4 +- .../useBackgroundQuery/testUtils.tsx | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx diff --git a/config/jest.config.ts b/config/jest.config.ts index accfbd82f84..c5b85fef821 100644 --- a/config/jest.config.ts +++ b/config/jest.config.ts @@ -49,6 +49,7 @@ const react17TestFileIgnoreList = [ "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", @@ -83,7 +84,8 @@ const standardReact19Config = { testPathIgnorePatterns: [ ignoreDTSFiles, ignoreTSFiles, - "src/react/hooks/__tests__/useSuspenseQuery/utils.tsx", + "src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx", + "src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx", ], }; diff --git a/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx b/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx new file mode 100644 index 00000000000..cc60c1fca69 --- /dev/null +++ b/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx @@ -0,0 +1,75 @@ +import type { RenderOptions } from "@testing-library/react"; +import { + createRenderStream, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { DataState, ErrorLike, OperationVariables } from "@apollo/client"; +import type { QueryRef, useBackgroundQuery } from "@apollo/client/react"; +import { useReadQuery } from "@apollo/client/react"; + +export async function renderSuspenseHook< + TData, + TVariables extends OperationVariables, + TQueryRef extends QueryRef, + TStates extends DataState["dataState"] = TQueryRef extends ( + QueryRef + ) ? + States + : never, + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => [TQueryRef | undefined, useBackgroundQuery.Result], + options: Pick & { initialProps?: Props } +) { + function UseReadQuery({ queryRef }: { queryRef: QueryRef }) { + useTrackRenders({ name: "useReadQuery" }); + replaceSnapshot(useReadQuery(queryRef) as any); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + + return null; + } + + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useBackgroundQuery" }); + const [queryRef] = renderHook(props as any); + + return ( + }> + replaceSnapshot({ error })} + > + {queryRef && } + + + ); + } + + const { render, takeRender, replaceSnapshot } = createRenderStream< + useReadQuery.Result | { error: ErrorLike } + >(); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + return { takeRender, rerender }; +} From e6855c5f54829a3634e28dacf4e7924652d4dc72 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 14:35:09 -0600 Subject: [PATCH 16/33] Move new tests to own file --- .../__tests__/useBackgroundQuery.test.tsx | 282 +---------------- .../useBackgroundQuery/skipToken.test.tsx | 290 ++++++++++++++++++ 2 files changed, 291 insertions(+), 281 deletions(-) create mode 100644 src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 15d470e5d20..647d4ca3ff2 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -12,7 +12,7 @@ import { GraphQLError } from "graphql"; import React, { Suspense } from "react"; import type { FallbackProps } from "react-error-boundary"; import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; -import { delay, Observable, of } from "rxjs"; +import { Observable, of } from "rxjs"; import type { DataState, @@ -1969,286 +1969,6 @@ it("renders skip result, does not suspend, and maintains `data` when switching b await expect(renderStream).not.toRerender({ timeout: 50 }); }); -// 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 user = userEvent.setup(); - const renderStream = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - 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(), - }); - - function App() { - useTrackRenders(); - const [skip, setSkip] = React.useState(false); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : { variables: { id: "1" } } - ); - - return ( - <> - - }> - {queryRef && } - - - ); - } - - using _disabledAct = disableActEnvironment(); - await renderStream.render(, { wrapper: createClientWrapper(client) }); - - { - const { renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } - - { - const { snapshot } = await renderStream.takeRender(); - - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await user.click(screen.getByText("Toggle skip")); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender(); - - await expect( - client.refetchQueries({ include: [query] }) - ).resolves.toStrictEqualTyped([ - { - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - }, - ]); - - await expect(renderStream).not.toRerender(); -}); - -test("suspends and fetches when changing variables when no longer using skipToken", async () => { - const { query, mocks } = setupVariablesCase(); - const renderStream = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - function App({ id }: { id: string | undefined }) { - useTrackRenders(); - const [queryRef] = useBackgroundQuery( - query, - id === undefined ? skipToken : { variables: { id } } - ); - - return ( - <> - }> - {queryRef && } - - - ); - } - - using _disabledAct = disableActEnvironment(); - const { rerender } = await renderStream.render(, { - wrapper: createMockWrapper({ mocks }), - }); - - { - const { renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } - - { - const { snapshot } = await renderStream.takeRender(); - - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await rerender(); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await rerender(); - - { - const { renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } - - { - const { snapshot } = await renderStream.takeRender(); - - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "2", name: "Black Widow" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).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 renderStream = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query, - data: { - character: { __typename: "Character", id: "2", name: "Cached Widow" }, - }, - variables: { id: "2" }, - }); - - function App({ id }: { id: string | undefined }) { - useTrackRenders(); - const [queryRef] = useBackgroundQuery( - query, - id === undefined ? skipToken : { variables: { id } } - ); - - return ( - <> - }> - {queryRef && } - - - ); - } - - using _disabledAct = disableActEnvironment(); - const { rerender } = await renderStream.render(, { - wrapper: createMockWrapper({ cache, mocks }), - }); - - { - const { renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } - - { - const { snapshot } = await renderStream.takeRender(); - - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await rerender(); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "1", name: "Spider-Man" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await rerender(); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - character: { __typename: "Character", id: "2", name: "Cached Widow" }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender(); -}); - it("does not make network requests when `skip` is `true`", async () => { const { query, mocks } = setupSimpleCase(); const user = userEvent.setup(); diff --git a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx new file mode 100644 index 00000000000..34cd2cc34a8 --- /dev/null +++ b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx @@ -0,0 +1,290 @@ +import { disableActEnvironment } from "@testing-library/react-render-stream"; +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 { renderSuspenseHook } 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 renderSuspenseHook( + ({ 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", + "SuspenseFallback", + ]); + } + + { + 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.only("suspends and fetches when changing variables when no longer using skipToken", async () => { + const { query, mocks } = setupVariablesCase(); + + using _disabledAct = disableActEnvironment(); + const { rerender, takeRender } = await renderSuspenseHook( + ({ 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", + "SuspenseFallback", + ]); + } + + { + 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", + "SuspenseFallback", + ]); + } + + { + 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 renderSuspenseHook( + ({ 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", + "SuspenseFallback", + ]); + } + + { + 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(); +}); From 936789bcabcc5565233791496ab6184bc2fcee8e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 14:50:03 -0600 Subject: [PATCH 17/33] Allow custom delay for setupVariablesCase --- src/testing/internal/scenarios/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index 8b0d48ee69f..32f86aaddef 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -38,7 +38,9 @@ export interface VariablesCaseVariables { id: string; } -export function setupVariablesCase() { +export function setupVariablesCase({ + delay = 20, +}: { delay?: MockLink.Delay } = {}) { const query: TypedDocumentNode = gql` query CharacterQuery($id: ID!) { @@ -60,7 +62,7 @@ export function setupVariablesCase() { character: { __typename: "Character", id: String(index + 1), name }, }, }, - delay: 20, + delay, })); return { mocks, query }; From 961bc0405d980147ac6a69e7d27a12791afff333 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 14:50:17 -0600 Subject: [PATCH 18/33] Fix issue in React 18 with tests --- .../useBackgroundQuery/skipToken.test.tsx | 9 +++-- .../hooks/__tests__/useSuspenseQuery.test.tsx | 34 +++++++++---------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx index 34cd2cc34a8..e014dc130bc 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx @@ -1,4 +1,5 @@ import { disableActEnvironment } from "@testing-library/react-render-stream"; +import React from "react"; import { delay, of } from "rxjs"; import { @@ -114,8 +115,12 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer await expect(takeRender).not.toRerender(); }); -test.only("suspends and fetches when changing variables when no longer using skipToken", async () => { - const { query, mocks } = setupVariablesCase(); +test("suspends and fetches when changing variables when no longer using skipToken", async () => { + const { query, mocks } = setupVariablesCase({ + // React 18 needs a longer delay to commit the render when unskipping + // for some reason + delay: React.version.startsWith("18") ? 200 : 20, + }); using _disabledAct = disableActEnvironment(); const { rerender, takeRender } = await renderSuspenseHook( diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 6ce978c67a5..cdb9bc037bd 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -13,7 +13,7 @@ import { GraphQLError } from "graphql"; import React, { Fragment, StrictMode, Suspense, useTransition } from "react"; import type { FallbackProps } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary"; -import { Observable, of } from "rxjs"; +import { delay, Observable, of } from "rxjs"; import type { ApolloCache, @@ -306,7 +306,10 @@ function useVariablesQueryCase() { character: { __typename: "Character", id: String(index + 1), name }, }, }, - delay: 20, + // React runs layout effects much later in React 18 which means tracked + // components aren't captured correctly, specifically when changing + // variables that cause the component to suspend. + delay: IS_REACT_19 ? 20 : 200, })); return { query, mocks }; @@ -1729,7 +1732,7 @@ describe("useSuspenseQuery", () => { character: { __typename: "Character", id: "1", name: "Spider-Man" }, }, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "2" } }, @@ -1742,7 +1745,7 @@ describe("useSuspenseQuery", () => { }, }, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "1" } }, @@ -1755,7 +1758,7 @@ describe("useSuspenseQuery", () => { }, }, }, - delay: 20, + delay: 200, }, ]; @@ -1855,7 +1858,7 @@ describe("useSuspenseQuery", () => { character: { __typename: "Character", id: "1", name: "Spider-Man" }, }, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "2" } }, @@ -1868,7 +1871,7 @@ describe("useSuspenseQuery", () => { }, }, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "1" } }, @@ -1881,7 +1884,7 @@ describe("useSuspenseQuery", () => { }, }, }, - delay: 20, + delay: 200, }, ]; @@ -1975,7 +1978,7 @@ describe("useSuspenseQuery", () => { character: { __typename: "Character", id: "1", name: "Spider-Man" }, }, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "2" } }, @@ -1988,7 +1991,7 @@ describe("useSuspenseQuery", () => { }, }, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "1" } }, @@ -2001,7 +2004,7 @@ describe("useSuspenseQuery", () => { }, }, }, - delay: 20, + delay: 200, }, ]; @@ -3534,10 +3537,7 @@ describe("useSuspenseQuery", () => { const client = new ApolloClient({ cache: new InMemoryCache(), link: new ApolloLink((operation) => { - return new Observable((observer) => { - observer.next({ data: { vars: operation.variables } }); - observer.complete(); - }); + return of({ data: { vars: operation.variables } }).pipe(delay(200)); }), defaultOptions: { watchQuery: { @@ -4385,14 +4385,14 @@ describe("useSuspenseQuery", () => { result: { errors: graphQLErrors, }, - delay: 20, + delay: 200, }, { request: { query, variables: { id: "2" } }, result: { data: { user: { id: "2", name: "Captain Marvel" } }, }, - delay: 20, + delay: 200, }, ]; From a91b1de73d4428c8bfd8dd0ed483b7ba4d012986 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 15:33:52 -0600 Subject: [PATCH 19/33] Fix ignore path --- config/jest.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/jest.config.ts b/config/jest.config.ts index c5b85fef821..aa61aec141c 100644 --- a/config/jest.config.ts +++ b/config/jest.config.ts @@ -96,7 +96,8 @@ const standardReact18Config = { ignoreDTSFiles, ignoreTSFiles, "src/react/ssr/__tests__/prerenderStatic.test.tsx", - "src/react/hooks/__tests__/useSuspenseQuery/utils.tsx", + "src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx", + "src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx", ], moduleNameMapper: { ...defaults.moduleNameMapper, From e84a075946b23fa4ae72f3021a4900ad5e4155a1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 15:38:04 -0600 Subject: [PATCH 20/33] Create shared ignore list --- config/jest.config.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/config/jest.config.ts b/config/jest.config.ts index aa61aec141c..b7996a9a3b5 100644 --- a/config/jest.config.ts +++ b/config/jest.config.ts @@ -39,9 +39,15 @@ const ignoreDTSFiles = ".d.ts$"; const ignoreTSFiles = ".ts$"; const ignoreTSXFiles = ".tsx$"; -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", @@ -81,23 +87,15 @@ const tsRxJSMinConfig = { const standardReact19Config = { ...defaults, displayName: "ReactDOM 19", - testPathIgnorePatterns: [ - ignoreDTSFiles, - ignoreTSFiles, - "src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx", - "src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx", - ], + testPathIgnorePatterns: reactSharedTestFileIgnoreList, }; const standardReact18Config = { ...defaults, displayName: "ReactDOM 18", testPathIgnorePatterns: [ - ignoreDTSFiles, - ignoreTSFiles, + ...reactSharedTestFileIgnoreList, "src/react/ssr/__tests__/prerenderStatic.test.tsx", - "src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx", - "src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx", ], moduleNameMapper: { ...defaults.moduleNameMapper, From d541c2654df493ddffddf2848c7d41eeff579cfc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 15:39:02 -0600 Subject: [PATCH 21/33] Add longer delay to skipToken tests --- src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx index d24596b0103..8bc1af087f0 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx @@ -109,7 +109,7 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer }); test("suspends and fetches when changing variables when no longer using skipToken", async () => { - const { query, mocks } = setupVariablesCase(); + const { query, mocks } = setupVariablesCase({ delay: 200 }); using _disabledAct = disableActEnvironment(); const { takeRender, rerender } = await renderSuspenseHook( From 68a4a22b36eabe34a1634f24a2f59705bef9bd79 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 15:53:04 -0600 Subject: [PATCH 22/33] Extract cache key to hook --- src/react/hooks/internal/index.ts | 1 + .../hooks/internal/useSuspenseHookCacheKey.ts | 46 +++++++++++++++++++ src/react/hooks/useBackgroundQuery.ts | 30 ++---------- src/react/hooks/useSuspenseQuery.ts | 34 ++++---------- 4 files changed, 58 insertions(+), 53 deletions(-) create mode 100644 src/react/hooks/internal/useSuspenseHookCacheKey.ts diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts index f930bd3998c..301a51269a6 100644 --- a/src/react/hooks/internal/index.ts +++ b/src/react/hooks/internal/index.ts @@ -1,5 +1,6 @@ // These hooks are used internally and are not exported publicly by the library export { useDeepMemo } from "./useDeepMemo.js"; export { useRenderGuard } from "./useRenderGuard.js"; +export { useSuspenseHookCacheKey } from "./useSuspenseHookCacheKey.js"; export { __use } from "./__use.js"; export { wrapHook } from "./wrapHook.js"; diff --git a/src/react/hooks/internal/useSuspenseHookCacheKey.ts b/src/react/hooks/internal/useSuspenseHookCacheKey.ts new file mode 100644 index 00000000000..4ad75d89503 --- /dev/null +++ b/src/react/hooks/internal/useSuspenseHookCacheKey.ts @@ -0,0 +1,46 @@ +import type { DocumentNode } from "graphql"; +import * as React from "react"; + +import type { OperationVariables } from "@apollo/client"; +import type { CacheKey } from "@apollo/client/react/internal"; +import { canonicalStringify } from "@apollo/client/utilities"; + +import type { SkipToken } from "../constants.js"; +import { skipToken } from "../constants.js"; + +export declare namespace useSuspenseHookCacheKey { + export interface Options { + variables?: OperationVariables; + queryKey?: string | number | any[]; + } +} + +export function useSuspenseHookCacheKey( + query: DocumentNode, + options: + | (SkipToken & Partial) + | useSuspenseHookCacheKey.Options +) { + const { queryKey = [], variables } = options; + const canonicalVariables = canonicalStringify(variables); + + // This state value let's us maintain the variables used for the cache key + // when `skipToken` is used to skip a query after its been executed. + // Since options are provided when using `skipToken`, `variables` disappear, + // which means a cache key without a variables value is used to create a new + // `ObservableQuery` instance. This was particularly problematic when + // `refetchQueries` was used because it meant refetching against an + // `ObservableQuery` instance that had no variables. + let [cacheKeyVariables, setCacheKeyVariables] = + React.useState(canonicalVariables); + + if (options !== skipToken && cacheKeyVariables !== canonicalVariables) { + setCacheKeyVariables((cacheKeyVariables = canonicalVariables)); + } + + return [ + query, + cacheKeyVariables, + ...([] as any[]).concat(queryKey), + ] satisfies CacheKey; +} diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 79b82317e38..88f98543d87 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -12,10 +12,8 @@ import type { WatchQueryFetchPolicy, } from "@apollo/client"; import type { SubscribeToMoreFunction } from "@apollo/client"; -import { canonicalStringify } from "@apollo/client/cache"; import type { QueryRef } from "@apollo/client/react"; import type { - CacheKey, FetchMoreFunction, RefetchFunction, } from "@apollo/client/react/internal"; @@ -32,8 +30,7 @@ import type { } from "@apollo/client/utilities/internal"; import type { SkipToken } from "./constants.js"; -import { skipToken } from "./constants.js"; -import { wrapHook } from "./internal/index.js"; +import { useSuspenseHookCacheKey, wrapHook } from "./internal/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; @@ -450,23 +447,8 @@ function useBackgroundQuery_< const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); const watchQueryOptions = useWatchQueryOptions({ client, query, options }); - const { fetchPolicy, variables } = watchQueryOptions; - const { queryKey = [] } = options; - const canonicalVariables = canonicalStringify(variables); - - // This state value let's us maintain the variables used for the cache key - // when `skipToken` is used to skip a query after its been executed. - // Since options are provided when using `skipToken`, `variables` disappear, - // which means a cache key without a variables value is used to create a new - // `ObservableQuery` instance. This was particularly problematic when - // `refetchQueries` was used because it meant refetching against an - // `ObservableQuery` instance that had no variables. - let [cacheKeyVariables, setCacheKeyVariables] = - React.useState(canonicalVariables); - - if (options !== skipToken && cacheKeyVariables !== canonicalVariables) { - setCacheKeyVariables((cacheKeyVariables = canonicalVariables)); - } + const { fetchPolicy } = watchQueryOptions; + const cacheKey = useSuspenseHookCacheKey(query, options); // This ref tracks the first time query execution is enabled to determine // whether to return a query ref or `undefined`. When initialized @@ -477,12 +459,6 @@ function useBackgroundQuery_< const didFetchResult = React.useRef(fetchPolicy !== "standby"); didFetchResult.current ||= fetchPolicy !== "standby"; - const cacheKey: CacheKey = [ - query, - cacheKeyVariables, - ...([] as any[]).concat(queryKey), - ]; - const queryRef = suspenseCache.getQueryRef(cacheKey, () => client.watchQuery( watchQueryOptions as ApolloClient.WatchQueryOptions diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 5b061628930..4e41f801859 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -17,9 +17,7 @@ import type { } from "@apollo/client"; import type { SubscribeToMoreFunction } from "@apollo/client"; import { NetworkStatus } from "@apollo/client"; -import { canonicalStringify } from "@apollo/client/cache"; import type { - CacheKey, FetchMoreFunction, QueryKey, RefetchFunction, @@ -34,7 +32,12 @@ import type { import type { SkipToken } from "./constants.js"; import { skipToken } from "./constants.js"; -import { __use, useDeepMemo, wrapHook } from "./internal/index.js"; +import { + __use, + useDeepMemo, + useSuspenseHookCacheKey, + wrapHook, +} from "./internal/index.js"; import { validateSuspenseHookOptions } from "./internal/validateSuspenseHookOptions.js"; import { useApolloClient } from "./useApolloClient.js"; @@ -375,29 +378,8 @@ function useSuspenseQuery_< query, options, }); - const { fetchPolicy, variables } = watchQueryOptions; - const { queryKey = [] } = options; - const canonicalVariables = canonicalStringify(variables); - - // This state value let's us maintain the variables used for the cache key - // when `skipToken` is used to skip a query after its been executed. - // Since options are provided when using `skipToken`, `variables` disappear, - // which means a cache key without a variables value is used to create a new - // `ObservableQuery` instance. This was particularly problematic when - // `refetchQueries` was used because it meant refetching against an - // `ObservableQuery` instance that had no variables. - let [cacheKeyVariables, setCacheKeyVariables] = - React.useState(canonicalVariables); - - if (options !== skipToken && cacheKeyVariables !== canonicalVariables) { - setCacheKeyVariables((cacheKeyVariables = canonicalVariables)); - } - - const cacheKey: CacheKey = [ - query, - cacheKeyVariables, - ...([] as any[]).concat(queryKey), - ]; + const { fetchPolicy } = watchQueryOptions; + const cacheKey = useSuspenseHookCacheKey(query, options); const queryRef = suspenseCache.getQueryRef(cacheKey, () => client.watchQuery(watchQueryOptions) From c044493fc9bae8ffaf57b203aa3d07b29dc097e1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 15:55:58 -0600 Subject: [PATCH 23/33] Remove comment --- src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx index e014dc130bc..b45b911338f 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx @@ -117,8 +117,6 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer test("suspends and fetches when changing variables when no longer using skipToken", async () => { const { query, mocks } = setupVariablesCase({ - // React 18 needs a longer delay to commit the render when unskipping - // for some reason delay: React.version.startsWith("18") ? 200 : 20, }); From 738a714acb0fc88bbda12c6010f4c188c9f2de54 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 15:59:17 -0600 Subject: [PATCH 24/33] Fix formatting --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index f5b60a4a293..1a3cc5ac991 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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` From 1db3a3611befb5b176c32a87c6600628bb09ed04 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 16:40:02 -0600 Subject: [PATCH 25/33] Add patch for type --- ...ng-library+react-render-stream+2.0.0.patch | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 patches/@testing-library+react-render-stream+2.0.0.patch diff --git a/patches/@testing-library+react-render-stream+2.0.0.patch b/patches/@testing-library+react-render-stream+2.0.0.patch new file mode 100644 index 00000000000..c85563cded1 --- /dev/null +++ b/patches/@testing-library+react-render-stream+2.0.0.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/@testing-library/react-render-stream/dist/pure.d.cts b/node_modules/@testing-library/react-render-stream/dist/pure.d.cts +index 8964c27..8875af7 100644 +--- a/node_modules/@testing-library/react-render-stream/dist/pure.d.cts ++++ b/node_modules/@testing-library/react-render-stream/dist/pure.d.cts +@@ -306,4 +306,4 @@ declare namespace disableActEnvironment { + var defaultOptions: Required; + } + +-export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; ++export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type Render, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; +diff --git a/node_modules/@testing-library/react-render-stream/dist/pure.d.ts b/node_modules/@testing-library/react-render-stream/dist/pure.d.ts +index 8964c27..8875af7 100644 +--- a/node_modules/@testing-library/react-render-stream/dist/pure.d.ts ++++ b/node_modules/@testing-library/react-render-stream/dist/pure.d.ts +@@ -306,4 +306,4 @@ declare namespace disableActEnvironment { + var defaultOptions: Required; + } + +-export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; ++export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type Render, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; From 90b6791de66fe713ff2d3b4f2b869c3650c4f97c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 16:52:14 -0600 Subject: [PATCH 26/33] Rename helper functions for clarity --- .../useBackgroundQuery/skipToken.test.tsx | 8 +++--- .../useBackgroundQuery/testUtils.tsx | 2 +- .../hooks/__tests__/useSuspenseQuery.test.tsx | 26 +++++++++---------- .../useSuspenseQuery/skipToken.test.tsx | 8 +++--- .../__tests__/useSuspenseQuery/testUtils.tsx | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx index b45b911338f..ee5819249c4 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx @@ -15,7 +15,7 @@ import { setupVariablesCase, } from "@apollo/client/testing/internal"; -import { renderSuspenseHook } from "./testUtils.js"; +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 () => { @@ -46,7 +46,7 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderSuspenseHook( + const { rerender, takeRender } = await renderUseBackgroundQuery( ({ id }) => useBackgroundQuery( query, @@ -121,7 +121,7 @@ test("suspends and fetches when changing variables when no longer using skipToke }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderSuspenseHook( + const { rerender, takeRender } = await renderUseBackgroundQuery( ({ id }) => useBackgroundQuery( query, @@ -216,7 +216,7 @@ test("does not suspend for data in the cache when changing variables when no lon }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderSuspenseHook( + const { rerender, takeRender } = await renderUseBackgroundQuery( ({ id }) => useBackgroundQuery( query, diff --git a/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx b/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx index cc60c1fca69..0de8f488a61 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx @@ -10,7 +10,7 @@ import type { DataState, ErrorLike, OperationVariables } from "@apollo/client"; import type { QueryRef, useBackgroundQuery } from "@apollo/client/react"; import { useReadQuery } from "@apollo/client/react"; -export async function renderSuspenseHook< +export async function renderUseBackgroundQuery< TData, TVariables extends OperationVariables, TQueryRef extends QueryRef, diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index cdb9bc037bd..df768d09735 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -72,7 +72,7 @@ import type { WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions.js"; -import { renderSuspenseHook as renderUseSuspenseQueryHook } from "./useSuspenseQuery/testUtils.js"; +import { renderUseSuspenseQuery } from "./useSuspenseQuery/testUtils.js"; const IS_REACT_19 = React.version.startsWith("19"); @@ -1098,7 +1098,7 @@ describe("useSuspenseQuery", () => { const { query, mocks } = useVariablesQueryCase(); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { variables: { id } }), { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); @@ -1581,7 +1581,7 @@ describe("useSuspenseQuery", () => { }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { variables: { id } }), { wrapper: createClientWrapper(client), initialProps: { id: "1" } } ); @@ -1649,7 +1649,7 @@ describe("useSuspenseQuery", () => { const { query, mocks } = useVariablesQueryCase(); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "cache-first", @@ -1763,7 +1763,7 @@ describe("useSuspenseQuery", () => { ]; using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "cache-and-network", @@ -1889,7 +1889,7 @@ describe("useSuspenseQuery", () => { ]; using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "network-only", @@ -2009,7 +2009,7 @@ describe("useSuspenseQuery", () => { ]; using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { fetchPolicy: "no-cache", @@ -2088,7 +2088,7 @@ describe("useSuspenseQuery", () => { }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { variables: { id } }), { wrapper: createClientWrapper(client), initialProps: { id: "1" } } ); @@ -2386,7 +2386,7 @@ describe("useSuspenseQuery", () => { }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(fullQuery, { fetchPolicy: "cache-first", @@ -2875,7 +2875,7 @@ describe("useSuspenseQuery", () => { }); using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(fullQuery, { fetchPolicy: "cache-and-network", @@ -3092,7 +3092,7 @@ describe("useSuspenseQuery", () => { const { query, mocks } = useVariablesQueryCase(); using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderUseSuspenseQueryHook( + const { takeRender, rerender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { fetchPolicy, variables: { id } }), { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } ); @@ -3547,7 +3547,7 @@ describe("useSuspenseQuery", () => { }); using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderUseSuspenseQueryHook( + const { takeRender, rerender } = await renderUseSuspenseQuery( ({ source }) => useSuspenseQuery(query, { fetchPolicy: "network-only", @@ -4397,7 +4397,7 @@ describe("useSuspenseQuery", () => { ]; using _disabledAct = disableActEnvironment(); - const { rerender, takeRender } = await renderUseSuspenseQueryHook( + const { rerender, takeRender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery(query, { errorPolicy: "all", variables: { id } }), { wrapper: createMockWrapper({ mocks }), initialProps: { id: "1" } } diff --git a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx index 8bc1af087f0..cd14241f70d 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx @@ -15,7 +15,7 @@ import { setupVariablesCase, } from "@apollo/client/testing/internal"; -import { renderSuspenseHook } from "./testUtils.js"; +import { renderUseSuspenseQuery } 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 () => { @@ -46,7 +46,7 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer }); using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderSuspenseHook( + const { takeRender, rerender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery( query, @@ -112,7 +112,7 @@ test("suspends and fetches when changing variables when no longer using skipToke const { query, mocks } = setupVariablesCase({ delay: 200 }); using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderSuspenseHook( + const { takeRender, rerender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery( query, @@ -202,7 +202,7 @@ test("does not suspend for data in the cache when changing variables when no lon }); using _disabledAct = disableActEnvironment(); - const { takeRender, rerender } = await renderSuspenseHook( + const { takeRender, rerender } = await renderUseSuspenseQuery( ({ id }) => useSuspenseQuery( query, diff --git a/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx b/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx index a28cf4b1136..d7f4887254b 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx @@ -10,7 +10,7 @@ import type { ErrorLike, OperationVariables } from "@apollo/client"; import type { useSuspenseQuery } from "@apollo/client/react"; import { invariant } from "@apollo/client/utilities/invariant"; -export async function renderSuspenseHook< +export async function renderUseSuspenseQuery< TData, TVariables extends OperationVariables, Props = never, From a9b484d77b951299b3ec4446757e1d1416ba8f47 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 16:56:11 -0600 Subject: [PATCH 27/33] Rename component names in snapshots --- .../useBackgroundQuery/skipToken.test.tsx | 8 ++-- .../useBackgroundQuery/testUtils.tsx | 4 +- .../hooks/__tests__/useSuspenseQuery.test.tsx | 48 +++++++++---------- .../useSuspenseQuery/skipToken.test.tsx | 8 ++-- .../__tests__/useSuspenseQuery/testUtils.tsx | 4 +- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx index ee5819249c4..f70e6e8e414 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/skipToken.test.tsx @@ -63,7 +63,7 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer expect(renderedComponents).toStrictEqual([ "useBackgroundQuery", - "SuspenseFallback", + "", ]); } @@ -138,7 +138,7 @@ test("suspends and fetches when changing variables when no longer using skipToke expect(renderedComponents).toStrictEqual([ "useBackgroundQuery", - "SuspenseFallback", + "", ]); } @@ -182,7 +182,7 @@ test("suspends and fetches when changing variables when no longer using skipToke expect(renderedComponents).toStrictEqual([ "useBackgroundQuery", - "SuspenseFallback", + "", ]); } @@ -233,7 +233,7 @@ test("does not suspend for data in the cache when changing variables when no lon expect(renderedComponents).toStrictEqual([ "useBackgroundQuery", - "SuspenseFallback", + "", ]); } diff --git a/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx b/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx index 0de8f488a61..e1d34750a5b 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/testUtils.tsx @@ -34,13 +34,13 @@ export async function renderUseBackgroundQuery< } function SuspenseFallback() { - useTrackRenders({ name: "SuspenseFallback" }); + useTrackRenders({ name: "" }); return null; } function ErrorFallback() { - useTrackRenders({ name: "ErrorBoundary" }); + useTrackRenders({ name: "" }); return null; } diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index df768d09735..c7ea239590f 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -1106,7 +1106,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1126,7 +1126,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1589,7 +1589,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1609,7 +1609,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1661,7 +1661,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1681,7 +1681,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1775,7 +1775,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1795,7 +1795,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1901,7 +1901,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1921,7 +1921,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -1941,7 +1941,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2021,7 +2021,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2041,7 +2041,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2061,7 +2061,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2096,7 +2096,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2116,7 +2116,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2428,7 +2428,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -2917,7 +2917,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -3100,7 +3100,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -3120,7 +3120,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -3562,7 +3562,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -3584,7 +3584,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -4408,7 +4408,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -4428,7 +4428,7 @@ describe("useSuspenseQuery", () => { { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { diff --git a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx index cd14241f70d..698f402312f 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/skipToken.test.tsx @@ -61,7 +61,7 @@ test("maintains variables when switching to `skipToken` and calling `refetchQuer { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -127,7 +127,7 @@ test("suspends and fetches when changing variables when no longer using skipToke { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -166,7 +166,7 @@ test("suspends and fetches when changing variables when no longer using skipToke { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { @@ -217,7 +217,7 @@ test("does not suspend for data in the cache when changing variables when no lon { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([""]); } { diff --git a/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx b/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx index d7f4887254b..8554c0f566e 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/testUtils.tsx @@ -26,13 +26,13 @@ export async function renderUseSuspenseQuery< } function SuspenseFallback() { - useTrackRenders({ name: "SuspenseFallback" }); + useTrackRenders({ name: "" }); return null; } function ErrorFallback() { - useTrackRenders({ name: "ErrorBoundary" }); + useTrackRenders({ name: "" }); return null; } From bb25972dd8791bf6bb023f71205615ff0395c2fa Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 17:01:59 -0600 Subject: [PATCH 28/33] Update deprecation message --- src/react/hooks/__tests__/useSuspenseQuery.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index c7ea239590f..6ddb4ce4d32 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -102,8 +102,8 @@ interface SimpleQueryData { /** * @deprecated - * Use `renderSuspenseHook` aliased to `renderUseSuspenseQueryHook` from utils - * which uses render streams instead of render counting. + * Use the `renderUseSuspenseQuery` helper from utils which uses render streams + * instead of function call render counting. */ async function renderSuspenseHook( render: (initialProps: Props) => Result, From 40eddcd2826e77ba044447c8cd6fe58404b29008 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 17:05:18 -0600 Subject: [PATCH 29/33] Adjust comment for typos and accuracy --- src/react/hooks/internal/useSuspenseHookCacheKey.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/react/hooks/internal/useSuspenseHookCacheKey.ts b/src/react/hooks/internal/useSuspenseHookCacheKey.ts index 4ad75d89503..f3c58fa9694 100644 --- a/src/react/hooks/internal/useSuspenseHookCacheKey.ts +++ b/src/react/hooks/internal/useSuspenseHookCacheKey.ts @@ -26,11 +26,11 @@ export function useSuspenseHookCacheKey( // This state value let's us maintain the variables used for the cache key // when `skipToken` is used to skip a query after its been executed. - // Since options are provided when using `skipToken`, `variables` disappear, - // which means a cache key without a variables value is used to create a new - // `ObservableQuery` instance. This was particularly problematic when - // `refetchQueries` was used because it meant refetching against an - // `ObservableQuery` instance that had no variables. + // Since options aren't provided when using `skipToken`, `variables` would + // otherwise disappear which means we'd return a new cache key without a + // variables value which creates a new `ObservableQuery` instance. This was + // particularly problematic when `refetchQueries` was used because it meant + // refetching against an `ObservableQuery` instance that had no variables. let [cacheKeyVariables, setCacheKeyVariables] = React.useState(canonicalVariables); From b6f09c8cff628167d062dda45c4b648ec8cf480e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 17:30:04 -0600 Subject: [PATCH 30/33] Add changeset --- .changeset/eleven-bikes-flash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eleven-bikes-flash.md diff --git a/.changeset/eleven-bikes-flash.md b/.changeset/eleven-bikes-flash.md new file mode 100644 index 00000000000..c68d174d1d4 --- /dev/null +++ b/.changeset/eleven-bikes-flash.md @@ -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`. From 09e72fcd98f99c47238681096818fd688a11e3b1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Oct 2025 17:47:18 -0600 Subject: [PATCH 31/33] Update size limits --- .size-limits.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index abf6288b711..4f116ec9282 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -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 } From 6ffc61169d147d85392ed122c051c82983eeba97 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 31 Oct 2025 10:47:40 -0600 Subject: [PATCH 32/33] Upgrade react-render-stream and remove patch --- package-lock.json | 6 ++++-- package.json | 2 +- ...ng-library+react-render-stream+2.0.0.patch | 20 ------------------- 3 files changed, 5 insertions(+), 23 deletions(-) delete mode 100644 patches/@testing-library+react-render-stream+2.0.0.patch diff --git a/package-lock.json b/package-lock.json index 3b31b717d01..89c275a5ddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,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", @@ -6189,7 +6189,9 @@ } }, "node_modules/@testing-library/react-render-stream": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@testing-library/react-render-stream/-/react-render-stream-2.0.2.tgz", + "integrity": "sha512-rZNWU6ECbqaplYoxxaD5+l4NRX49qxzCzZ8Sjbetw5JADchYAe+8h+TDy7G+1sAIUwevWbthE2sQfXyVfkTExg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index be5b0f1b757..cd6a327a6b5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/@testing-library+react-render-stream+2.0.0.patch b/patches/@testing-library+react-render-stream+2.0.0.patch deleted file mode 100644 index c85563cded1..00000000000 --- a/patches/@testing-library+react-render-stream+2.0.0.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/node_modules/@testing-library/react-render-stream/dist/pure.d.cts b/node_modules/@testing-library/react-render-stream/dist/pure.d.cts -index 8964c27..8875af7 100644 ---- a/node_modules/@testing-library/react-render-stream/dist/pure.d.cts -+++ b/node_modules/@testing-library/react-render-stream/dist/pure.d.cts -@@ -306,4 +306,4 @@ declare namespace disableActEnvironment { - var defaultOptions: Required; - } - --export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; -+export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type Render, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; -diff --git a/node_modules/@testing-library/react-render-stream/dist/pure.d.ts b/node_modules/@testing-library/react-render-stream/dist/pure.d.ts -index 8964c27..8875af7 100644 ---- a/node_modules/@testing-library/react-render-stream/dist/pure.d.ts -+++ b/node_modules/@testing-library/react-render-stream/dist/pure.d.ts -@@ -306,4 +306,4 @@ declare namespace disableActEnvironment { - var defaultOptions: Required; - } - --export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; -+export { type Assertable, type RenderWithoutActAsync as AsyncRenderFn, type DisableActEnvironmentOptions, type NextRenderOptions, type Render, type RenderStream, type RenderStreamOptions, type RenderStreamWithRenderFn, type SnapshotStream, type SyncScreen, WaitForRenderTimeoutError, cleanup, createRenderStream, disableActEnvironment, renderHookToSnapshotStream, useTrackRenders }; From 17abb5cd379098494ea6a1133f7557a99bb891fc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 31 Oct 2025 10:51:23 -0600 Subject: [PATCH 33/33] Fix type issues after upgrade --- src/react/hooks/__tests__/useLazyQuery.test.tsx | 9 +++------ src/react/hooks/__tests__/useQuery.test.tsx | 14 +++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index 978577b2cb2..fb5b2b2b570 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -2834,8 +2834,7 @@ describe("useLazyQuery Hook", () => { const [originalExecute] = getCurrentSnapshot(); countRef.current++; - // TODO: Update when https://github.com/testing-library/react-render-stream-testing-library/issues/13 is fixed - await rerender(undefined); + await rerender(); { const [, result] = await takeSnapshot(); @@ -2890,8 +2889,7 @@ describe("useLazyQuery Hook", () => { countRef.current++; - // TODO: Update when https://github.com/testing-library/react-render-stream-testing-library/issues/13 is fixed - await rerender(undefined); + await rerender(); { const [, result] = await takeSnapshot(); @@ -2950,8 +2948,7 @@ describe("useLazyQuery Hook", () => { trackClosureValue.mockClear(); countRef.current++; - // TODO: Update when https://github.com/testing-library/react-render-stream-testing-library/issues/13 is fixed - await rerender(undefined); + await rerender(); [execute] = getCurrentSnapshot(); expect(execute).toBe(originalExecute); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 2d544bb11ac..f038bcfdb5a 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -174,7 +174,7 @@ describe("useQuery Hook", () => { }); } - await rerender({ children: null }); + await rerender(); { const result = await takeSnapshot(); @@ -244,7 +244,7 @@ describe("useQuery Hook", () => { }); } - await rerender({ children: null }); + await rerender(); { const result = await takeSnapshot(); @@ -400,7 +400,7 @@ describe("useQuery Hook", () => { }); } - await rerender(undefined); + await rerender(); { const result = await takeSnapshot(); @@ -471,7 +471,7 @@ describe("useQuery Hook", () => { expect(subscribeToMore).toBe(result.subscribeToMore); } - await rerender(undefined); + await rerender(); { const result = await takeSnapshot(); @@ -1244,7 +1244,7 @@ describe("useQuery Hook", () => { }); } - await rerender({}); + await rerender(); { const [result0, result1] = await takeSnapshot(); @@ -4064,7 +4064,7 @@ describe("useQuery Hook", () => { }); } - await rerender(undefined); + await rerender(); { const result = await takeSnapshot(); @@ -8103,7 +8103,7 @@ describe("useQuery Hook", () => { variables: {}, }); - await rerender(undefined); + await rerender(); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ data: undefined,