Skip to content

Commit db8a04b

Browse files
authored
Prevent unhandled rejection for promise returned from mutate function (#12892)
1 parent 4d3fb77 commit db8a04b

File tree

4 files changed

+119
-62
lines changed

4 files changed

+119
-62
lines changed

.changeset/odd-frogs-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Prevent unhandled rejections from the promise returned by calling the `mutate` function from the `useMutation` hook.

.size-limits.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43867,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38749,
4-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33600,
5-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27651
2+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43807,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38705,
4+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33582,
5+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27649
66
}

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Defer20220824Handler } from "@apollo/client/incremental";
3030
import { BatchHttpLink } from "@apollo/client/link/batch-http";
3131
import { ApolloProvider, useMutation, useQuery } from "@apollo/client/react";
3232
import { MockLink, MockSubscriptionLink } from "@apollo/client/testing";
33-
import { spyOnConsole } from "@apollo/client/testing/internal";
33+
import { spyOnConsole, wait } from "@apollo/client/testing/internal";
3434
import { MockedProvider } from "@apollo/client/testing/react";
3535
import type { DeepPartial } from "@apollo/client/utilities";
3636
import { invariant } from "@apollo/client/utilities/invariant";
@@ -513,6 +513,47 @@ describe("useMutation Hook", () => {
513513
await expect(takeSnapshot).not.toRerender();
514514
});
515515

516+
it("does not cause unhandled rejections", async () => {
517+
const variables = {
518+
description: "Get milk!",
519+
};
520+
521+
const mocks = [
522+
{
523+
request: {
524+
query: CREATE_TODO_MUTATION,
525+
variables,
526+
},
527+
result: {
528+
errors: [{ message: CREATE_TODO_ERROR }],
529+
},
530+
delay: 10,
531+
},
532+
];
533+
534+
using _disabledAct = disableActEnvironment();
535+
const { takeSnapshot, getCurrentSnapshot } =
536+
await renderHookToSnapshotStream(
537+
() => useMutation(CREATE_TODO_MUTATION, { variables }),
538+
{
539+
wrapper: ({ children }) => (
540+
<MockedProvider mocks={mocks}>{children}</MockedProvider>
541+
),
542+
}
543+
);
544+
545+
await takeSnapshot();
546+
547+
const [createTodo] = getCurrentSnapshot();
548+
549+
// Intentionally don't await this to ensure we don't get unhandled rejections
550+
createTodo();
551+
await wait(15);
552+
553+
// No assertions needed. This test fails if the promise throws an
554+
// unhandled rection
555+
});
556+
516557
it(`should reject when errorPolicy is 'none'`, async () => {
517558
const variables = {
518559
description: "Get milk!",

src/react/hooks/useMutation.ts

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import type {
2121
} from "@apollo/client";
2222
import type { IgnoreModifier } from "@apollo/client/cache";
2323
import type { NoInfer, Prettify } from "@apollo/client/utilities/internal";
24-
import { mergeOptions } from "@apollo/client/utilities/internal";
24+
import {
25+
mergeOptions,
26+
preventUnhandledRejection,
27+
} from "@apollo/client/utilities/internal";
2528

2629
import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js";
2730
import { useApolloClient } from "./useApolloClient.js";
@@ -284,72 +287,80 @@ export function useMutation<
284287
const mutationId = ++ref.current.mutationId;
285288
const clientOptions = mergeOptions(baseOptions, executeOptions as any);
286289

287-
return client
288-
.mutate(
289-
clientOptions as ApolloClient.MutateOptions<TData, OperationVariables>
290-
)
291-
.then(
292-
(response) => {
293-
const { data, error } = response;
290+
return preventUnhandledRejection(
291+
client
292+
.mutate(
293+
clientOptions as ApolloClient.MutateOptions<
294+
TData,
295+
OperationVariables
296+
>
297+
)
298+
.then(
299+
(response) => {
300+
const { data, error } = response;
301+
302+
const onError =
303+
executeOptions.onError || ref.current.options?.onError;
304+
305+
if (error && onError) {
306+
onError(error, clientOptions);
307+
}
294308

295-
const onError =
296-
executeOptions.onError || ref.current.options?.onError;
309+
if (mutationId === ref.current.mutationId) {
310+
const result = {
311+
called: true,
312+
loading: false,
313+
data,
314+
error,
315+
client,
316+
};
317+
318+
if (
319+
ref.current.isMounted &&
320+
!equal(ref.current.result, result)
321+
) {
322+
setResult((ref.current.result = result));
323+
}
324+
}
297325

298-
if (error && onError) {
299-
onError(error, clientOptions);
300-
}
326+
const onCompleted =
327+
executeOptions.onCompleted || ref.current.options?.onCompleted;
301328

302-
if (mutationId === ref.current.mutationId) {
303-
const result = {
304-
called: true,
305-
loading: false,
306-
data,
307-
error,
308-
client,
309-
};
310-
311-
if (ref.current.isMounted && !equal(ref.current.result, result)) {
312-
setResult((ref.current.result = result));
329+
if (!error) {
330+
onCompleted?.(response.data!, clientOptions);
313331
}
314-
}
315332

316-
const onCompleted =
317-
executeOptions.onCompleted || ref.current.options?.onCompleted;
333+
return response;
334+
},
335+
(error) => {
336+
if (
337+
mutationId === ref.current.mutationId &&
338+
ref.current.isMounted
339+
) {
340+
const result = {
341+
loading: false,
342+
error,
343+
data: void 0,
344+
called: true,
345+
client,
346+
};
347+
348+
if (!equal(ref.current.result, result)) {
349+
setResult((ref.current.result = result));
350+
}
351+
}
318352

319-
if (!error) {
320-
onCompleted?.(response.data!, clientOptions);
321-
}
353+
const onError =
354+
executeOptions.onError || ref.current.options?.onError;
322355

323-
return response;
324-
},
325-
(error) => {
326-
if (
327-
mutationId === ref.current.mutationId &&
328-
ref.current.isMounted
329-
) {
330-
const result = {
331-
loading: false,
332-
error,
333-
data: void 0,
334-
called: true,
335-
client,
336-
};
337-
338-
if (!equal(ref.current.result, result)) {
339-
setResult((ref.current.result = result));
356+
if (onError) {
357+
onError(error, clientOptions);
340358
}
341-
}
342-
343-
const onError =
344-
executeOptions.onError || ref.current.options?.onError;
345359

346-
if (onError) {
347-
onError(error, clientOptions);
360+
throw error;
348361
}
349-
350-
throw error;
351-
}
352-
);
362+
)
363+
);
353364
},
354365
[]
355366
);

0 commit comments

Comments
 (0)