Skip to content

Commit a3f95c6

Browse files
authored
Fix Suspense boundary indefinitely shown when fetchMore returns error (#12110)
1 parent f36b938 commit a3f95c6

File tree

4 files changed

+128
-3
lines changed

4 files changed

+128
-3
lines changed

.changeset/serious-cows-trade.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Fix an issue where errors returned from a `fetchMore` call from a Suspense hook would cause a Suspense boundary to be shown indefinitely.

.size-limits.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 40251,
2+
"dist/apollo-client.min.cjs": 40265,
33
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33060
44
}

src/react/hooks/__tests__/useSuspenseQuery.test.tsx

+121-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable testing-library/render-result-naming-convention */
12
import React, { Fragment, StrictMode, Suspense, useTransition } from "react";
23
import {
34
act,
@@ -8,7 +9,7 @@ import {
89
RenderHookOptions,
910
} from "@testing-library/react";
1011
import userEvent from "@testing-library/user-event";
11-
import { ErrorBoundary } from "react-error-boundary";
12+
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
1213
import { GraphQLError } from "graphql";
1314
import { InvariantError } from "ts-invariant";
1415
import { equal } from "@wry/equality";
@@ -10571,6 +10572,125 @@ describe("useSuspenseQuery", () => {
1057110572
await expect(renderStream).not.toRerender();
1057210573
});
1057310574

10575+
// https://github.com/apollographql/apollo-client/issues/12103
10576+
it("does not get stuck pending when `fetchMore` rejects with an error", async () => {
10577+
using _ = spyOnConsole("error");
10578+
const { query, data } = setupPaginatedCase();
10579+
10580+
const link = new ApolloLink((operation) => {
10581+
const { offset = 0, limit = 2 } = operation.variables;
10582+
const letters = data.slice(offset, offset + limit);
10583+
10584+
return new Observable((observer) => {
10585+
setTimeout(() => {
10586+
if (offset === 2) {
10587+
observer.next({
10588+
data: null,
10589+
errors: [{ message: "Could not fetch letters" }],
10590+
});
10591+
} else {
10592+
observer.next({ data: { letters } });
10593+
}
10594+
observer.complete();
10595+
}, 10);
10596+
});
10597+
});
10598+
10599+
const client = new ApolloClient({
10600+
cache: new InMemoryCache(),
10601+
link,
10602+
});
10603+
10604+
const renderStream = createRenderStream({
10605+
initialSnapshot: {
10606+
result: null as UseSuspenseQueryResult<
10607+
PaginatedCaseData,
10608+
PaginatedCaseVariables
10609+
> | null,
10610+
error: null as ApolloError | null,
10611+
},
10612+
});
10613+
10614+
function SuspenseFallback() {
10615+
useTrackRenders();
10616+
10617+
return <div>Loading...</div>;
10618+
}
10619+
10620+
function ErrorFallback({ error }: FallbackProps) {
10621+
useTrackRenders();
10622+
renderStream.mergeSnapshot({ error });
10623+
10624+
return <div>Error</div>;
10625+
}
10626+
10627+
function App() {
10628+
useTrackRenders();
10629+
const result = useSuspenseQuery(query, {
10630+
variables: { offset: 0, limit: 2 },
10631+
});
10632+
10633+
renderStream.mergeSnapshot({ result });
10634+
10635+
return null;
10636+
}
10637+
10638+
renderStream.render(
10639+
<Suspense fallback={<SuspenseFallback />}>
10640+
<ErrorBoundary FallbackComponent={ErrorFallback}>
10641+
<App />
10642+
</ErrorBoundary>
10643+
</Suspense>,
10644+
{
10645+
wrapper: ({ children }) => (
10646+
<ApolloProvider client={client}>{children}</ApolloProvider>
10647+
),
10648+
}
10649+
);
10650+
10651+
{
10652+
const { renderedComponents } = await renderStream.takeRender();
10653+
10654+
expect(renderedComponents).toStrictEqual([SuspenseFallback]);
10655+
}
10656+
10657+
{
10658+
const { snapshot, renderedComponents } = await renderStream.takeRender();
10659+
10660+
expect(renderedComponents).toStrictEqual([App]);
10661+
expect(snapshot.result?.data).toEqual({
10662+
letters: [
10663+
{ __typename: "Letter", letter: "A", position: 1 },
10664+
{ __typename: "Letter", letter: "B", position: 2 },
10665+
],
10666+
});
10667+
}
10668+
10669+
const { snapshot } = renderStream.getCurrentRender();
10670+
await act(() =>
10671+
snapshot.result!.fetchMore({ variables: { offset: 2 } }).catch(() => {})
10672+
);
10673+
10674+
{
10675+
const { renderedComponents } = await renderStream.takeRender();
10676+
10677+
expect(renderedComponents).toStrictEqual([SuspenseFallback]);
10678+
}
10679+
10680+
{
10681+
const { snapshot, renderedComponents } = await renderStream.takeRender();
10682+
10683+
expect(renderedComponents).toStrictEqual([ErrorFallback]);
10684+
expect(snapshot.error).toEqual(
10685+
new ApolloError({
10686+
graphQLErrors: [{ message: "Could not fetch letters" }],
10687+
})
10688+
);
10689+
}
10690+
10691+
await expect(renderStream).not.toRerender();
10692+
});
10693+
1057410694
describe.skip("type tests", () => {
1057510695
it("returns unknown when TData cannot be inferred", () => {
1057610696
const query = gql`

src/react/internal/cache/QueryReference.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ export class InternalQueryReference<TData = unknown> {
486486
}
487487
});
488488
})
489-
.catch(() => {});
489+
.catch((error) => this.reject?.(error));
490490

491491
return returnedPromise;
492492
}

0 commit comments

Comments
 (0)