diff --git a/.changeset/prompt-effect-order.md b/.changeset/prompt-effect-order.md new file mode 100644 index 0000000000..55280c255a --- /dev/null +++ b/.changeset/prompt-effect-order.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": patch +--- + +Reorder effects in `unstable_usePrompt` to avoid throwing an exception if the prompt is unblocked and a navigation is performed syncronously diff --git a/contributors.yml b/contributors.yml index 69cdb30698..47a2659440 100644 --- a/contributors.yml +++ b/contributors.yml @@ -223,3 +223,4 @@ - yuleicul - zheng-chuang - istarkov +- louis-young diff --git a/packages/react-router-dom/__tests__/use-prompt-test.tsx b/packages/react-router-dom/__tests__/use-prompt-test.tsx new file mode 100644 index 0000000000..64d8ce238f --- /dev/null +++ b/packages/react-router-dom/__tests__/use-prompt-test.tsx @@ -0,0 +1,126 @@ +import * as React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { + Link, + RouterProvider, + createBrowserRouter, + unstable_usePrompt as usePrompt, +} from "../index"; +import "@testing-library/jest-dom"; +import { JSDOM } from "jsdom"; + +describe("usePrompt", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("when navigation is blocked", () => { + it("shows window.confirm and blocks navigation when it returns false", async () => { + let testWindow = getWindowImpl("/"); + const windowConfirmMock = jest + .spyOn(window, "confirm") + .mockImplementationOnce(() => false); + + let router = createBrowserRouter( + [ + { + path: "/", + Component() { + usePrompt({ when: true, message: "Are you sure??" }); + return Navigate; + }, + }, + { + path: "/arbitrary", + Component: () =>

Arbitrary

, + }, + ], + { window: testWindow } + ); + + render(); + expect(screen.getByText("Navigate")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Navigate")); + await new Promise((r) => setTimeout(r, 0)); + + expect(windowConfirmMock).toHaveBeenNthCalledWith(1, "Are you sure??"); + expect(screen.getByText("Navigate")).toBeInTheDocument(); + }); + + it("shows window.confirm and navigates when it returns true", async () => { + let testWindow = getWindowImpl("/"); + const windowConfirmMock = jest + .spyOn(window, "confirm") + .mockImplementationOnce(() => true); + + let router = createBrowserRouter( + [ + { + path: "/", + Component() { + usePrompt({ when: true, message: "Are you sure??" }); + return Navigate; + }, + }, + { + path: "/arbitrary", + Component: () =>

Arbitrary

, + }, + ], + { window: testWindow } + ); + + render(); + expect(screen.getByText("Navigate")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Navigate")); + await waitFor(() => screen.getByText("Arbitrary")); + + expect(windowConfirmMock).toHaveBeenNthCalledWith(1, "Are you sure??"); + expect(screen.getByText("Arbitrary")).toBeInTheDocument(); + }); + }); + + describe("when navigation is not blocked", () => { + it("navigates without showing window.confirm", async () => { + let testWindow = getWindowImpl("/"); + const windowConfirmMock = jest + .spyOn(window, "confirm") + .mockImplementation(() => true); + + let router = createBrowserRouter( + [ + { + path: "/", + Component() { + usePrompt({ when: false, message: "Are you sure??" }); + return Navigate; + }, + }, + { + path: "/arbitrary", + Component: () =>

Arbitrary

, + }, + ], + { window: testWindow } + ); + + render(); + expect(screen.getByText("Navigate")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Navigate")); + await waitFor(() => screen.getByText("Arbitrary")); + + expect(windowConfirmMock).not.toHaveBeenCalled(); + expect(screen.getByText("Arbitrary")).toBeInTheDocument(); + }); + }); +}); + +function getWindowImpl(initialUrl: string, isHash = false): Window { + // Need to use our own custom DOM in order to get a working history + const dom = new JSDOM(``, { url: "http://localhost/" }); + dom.window.history.replaceState(null, "", (isHash ? "#" : "") + initialUrl); + return dom.window as unknown as Window; +} diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index d10b1e3d7e..cf12bca451 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -1461,22 +1461,22 @@ function usePageHide( function usePrompt({ when, message }: { when: boolean; message: string }) { let blocker = useBlocker(when); - React.useEffect(() => { - if (blocker.state === "blocked" && !when) { - blocker.reset(); - } - }, [blocker, when]); - React.useEffect(() => { if (blocker.state === "blocked") { let proceed = window.confirm(message); if (proceed) { - setTimeout(blocker.proceed, 0); + blocker.proceed(); } else { blocker.reset(); } } }, [blocker, message]); + + React.useEffect(() => { + if (blocker.state === "blocked" && !when) { + blocker.reset(); + } + }, [blocker, when]); } export { usePrompt as unstable_usePrompt };