Skip to content

Commit 28166c5

Browse files
Merge pull request #2275 from trilitech/async_handler
fix: avoid double toast by handleAsyncActionUnsafe
2 parents 12bc5ea + 04064fe commit 28166c5

File tree

2 files changed

+59
-15
lines changed

2 files changed

+59
-15
lines changed

packages/state/src/hooks/useAsyncActionHandler.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CustomError } from "@umami/utils";
2+
13
import { useAsyncActionHandler } from "./useAsyncActionHandler";
24
import { act, mockToast, renderHook, waitFor } from "../testUtils";
35

@@ -144,12 +146,12 @@ describe("useAsyncActionHandler", () => {
144146
describe("handleAsyncActionUnsafe", () => {
145147
it("returns the result of the computation", async () => {
146148
const view = fixture();
147-
148149
const result = await act(() =>
149150
view.result.current.handleAsyncActionUnsafe(() => Promise.resolve(42))
150151
);
151152

152153
expect(result).toBe(42);
154+
expect(mockToast).toHaveBeenCalledTimes(0);
153155
});
154156

155157
it("throws when the computation fails", async () => {
@@ -160,6 +162,41 @@ describe("useAsyncActionHandler", () => {
160162
view.result.current.handleAsyncActionUnsafe(() => Promise.reject(new Error("test error")))
161163
)
162164
).rejects.toThrow("test error");
165+
expect(mockToast).toHaveBeenCalledTimes(2);
166+
});
167+
168+
it("Unsafe propagates the error and shows the toast once on first handling", async () => {
169+
const view = fixture();
170+
171+
expect(mockToast).toHaveBeenCalledTimes(0);
172+
173+
const error: any = new CustomError("test nested error handling");
174+
await expect(
175+
act(() => view.result.current.handleAsyncActionUnsafe(() => Promise.reject(error)))
176+
).rejects.toThrow("test nested error handling");
177+
// check that error.processed is set to true
178+
expect(error.processed).toBe(true);
179+
expect(mockToast).toHaveBeenCalledWith({
180+
description: "test nested error handling",
181+
status: "error",
182+
isClosable: true,
183+
});
184+
expect(mockToast).toHaveBeenCalledTimes(2);
185+
});
186+
187+
it("Unsafe propagates the error and shows no toast on second handling", async () => {
188+
const view = fixture();
189+
190+
expect(mockToast).toHaveBeenCalledTimes(0);
191+
192+
const error: any = new CustomError("test nested error handling");
193+
error.processed = true;
194+
await expect(
195+
act(() => view.result.current.handleAsyncActionUnsafe(() => Promise.reject(error)))
196+
).rejects.toThrow("test nested error handling");
197+
// check that error.processed is still true
198+
expect(error.processed).toBe(true);
199+
expect(mockToast).toHaveBeenCalledTimes(0);
163200
});
164201
});
165202
});

packages/state/src/hooks/useAsyncActionHandler.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,29 @@ export const useAsyncActionHandler = () => {
4040
} catch (error: any) {
4141
const errorContext = getErrorContext(error);
4242

43-
toast({
44-
description: errorContext.description,
45-
status: "error",
46-
isClosable: true,
47-
...(typeof toastOptions === "function" ? toastOptions(error) : toastOptions),
48-
});
43+
// Prevent double toast and record of the same error if case of nested handleAsyncActionUnsafe calls.
44+
// Still we need to re-throw the error to propagate it to the caller.
45+
// There is no problem with handleAsyncAction calls as they stop the propagation of the error.
46+
if (!error.processed) {
47+
error.processed = true;
4948

50-
// TODO: fix this dirty hack
51-
mockToast({
52-
description: errorContext.description,
53-
status: "error",
54-
isClosable: true,
55-
...(typeof toastOptions === "function" ? toastOptions(error) : toastOptions),
56-
});
49+
toast({
50+
description: errorContext.description,
51+
status: "error",
52+
isClosable: true,
53+
...(typeof toastOptions === "function" ? toastOptions(error) : toastOptions),
54+
});
5755

58-
dispatch(errorsActions.add(errorContext));
56+
// TODO: fix this dirty hack
57+
mockToast({
58+
description: errorContext.description,
59+
status: "error",
60+
isClosable: true,
61+
...(typeof toastOptions === "function" ? toastOptions(error) : toastOptions),
62+
});
63+
64+
dispatch(errorsActions.add(errorContext));
65+
}
5966
throw error;
6067
} finally {
6168
isLoadingRef.current = false;

0 commit comments

Comments
 (0)