From a3f95c6f7623060bbf68b418b0ab268fabc0c9b6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Nov 2024 09:43:07 -0700 Subject: [PATCH] Fix Suspense boundary indefinitely shown when fetchMore returns error (#12110) --- .changeset/serious-cows-trade.md | 5 + .size-limits.json | 2 +- .../hooks/__tests__/useSuspenseQuery.test.tsx | 122 +++++++++++++++++- src/react/internal/cache/QueryReference.ts | 2 +- 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 .changeset/serious-cows-trade.md diff --git a/.changeset/serious-cows-trade.md b/.changeset/serious-cows-trade.md new file mode 100644 index 00000000000..a2fbbdd7713 --- /dev/null +++ b/.changeset/serious-cows-trade.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix an issue where errors returned from a `fetchMore` call from a Suspense hook would cause a Suspense boundary to be shown indefinitely. diff --git a/.size-limits.json b/.size-limits.json index 2ccf7b558b0..3b6e0211651 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40251, + "dist/apollo-client.min.cjs": 40265, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33060 } diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index a9d2c8d6b62..89621552d5c 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable testing-library/render-result-naming-convention */ import React, { Fragment, StrictMode, Suspense, useTransition } from "react"; import { act, @@ -8,7 +9,7 @@ import { RenderHookOptions, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ErrorBoundary } from "react-error-boundary"; +import { ErrorBoundary, FallbackProps } from "react-error-boundary"; import { GraphQLError } from "graphql"; import { InvariantError } from "ts-invariant"; import { equal } from "@wry/equality"; @@ -10571,6 +10572,125 @@ describe("useSuspenseQuery", () => { await expect(renderStream).not.toRerender(); }); + // https://github.com/apollographql/apollo-client/issues/12103 + it("does not get stuck pending when `fetchMore` rejects with an error", async () => { + using _ = spyOnConsole("error"); + const { query, data } = setupPaginatedCase(); + + const link = new ApolloLink((operation) => { + const { offset = 0, limit = 2 } = operation.variables; + const letters = data.slice(offset, offset + limit); + + return new Observable((observer) => { + setTimeout(() => { + if (offset === 2) { + observer.next({ + data: null, + errors: [{ message: "Could not fetch letters" }], + }); + } else { + observer.next({ data: { letters } }); + } + observer.complete(); + }, 10); + }); + }); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); + + const renderStream = createRenderStream({ + initialSnapshot: { + result: null as UseSuspenseQueryResult< + PaginatedCaseData, + PaginatedCaseVariables + > | null, + error: null as ApolloError | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + + return
Loading...
; + } + + function ErrorFallback({ error }: FallbackProps) { + useTrackRenders(); + renderStream.mergeSnapshot({ error }); + + return
Error
; + } + + function App() { + useTrackRenders(); + const result = useSuspenseQuery(query, { + variables: { offset: 0, limit: 2 }, + }); + + renderStream.mergeSnapshot({ result }); + + return null; + } + + renderStream.render( + }> + + + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result?.data).toEqual({ + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }); + } + + const { snapshot } = renderStream.getCurrentRender(); + await act(() => + snapshot.result!.fetchMore({ variables: { offset: 2 } }).catch(() => {}) + ); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [{ message: "Could not fetch letters" }], + }) + ); + } + + await expect(renderStream).not.toRerender(); + }); + describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { const query = gql` diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index b6279efd24c..76e264bf646 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -486,7 +486,7 @@ export class InternalQueryReference { } }); }) - .catch(() => {}); + .catch((error) => this.reject?.(error)); return returnedPromise; }