Skip to content

Commit

Permalink
Fix Suspense boundary indefinitely shown when fetchMore returns error (
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller authored Nov 7, 2024
1 parent f36b938 commit a3f95c6
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/serious-cows-trade.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion .size-limits.json
Original file line number Diff line number Diff line change
@@ -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
}
122 changes: 121 additions & 1 deletion src/react/hooks/__tests__/useSuspenseQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable testing-library/render-result-naming-convention */
import React, { Fragment, StrictMode, Suspense, useTransition } from "react";
import {
act,
Expand All @@ -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";
Expand Down Expand Up @@ -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 <div>Loading...</div>;
}

function ErrorFallback({ error }: FallbackProps) {
useTrackRenders();
renderStream.mergeSnapshot({ error });

return <div>Error</div>;
}

function App() {
useTrackRenders();
const result = useSuspenseQuery(query, {
variables: { offset: 0, limit: 2 },
});

renderStream.mergeSnapshot({ result });

return null;
}

renderStream.render(
<Suspense fallback={<SuspenseFallback />}>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<App />
</ErrorBoundary>
</Suspense>,
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
}
);

{
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`
Expand Down
2 changes: 1 addition & 1 deletion src/react/internal/cache/QueryReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ export class InternalQueryReference<TData = unknown> {
}
});
})
.catch(() => {});
.catch((error) => this.reject?.(error));

return returnedPromise;
}
Expand Down

0 comments on commit a3f95c6

Please sign in to comment.