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 };