Skip to content

Commit

Permalink
fix(react-router-dom): Fix usePrompt invalid blocker state transiti…
Browse files Browse the repository at this point in the history
…on (#10687)

Co-authored-by: Matt Brophy <matt@brophy.org>
  • Loading branch information
louis-young and brophdawg11 committed Jul 13, 2023
1 parent 1a68ac1 commit 1b8ee16
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/prompt-effect-order.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,4 @@
- yuleicul
- zheng-chuang
- istarkov
- louis-young
126 changes: 126 additions & 0 deletions packages/react-router-dom/__tests__/use-prompt-test.tsx
Original file line number Diff line number Diff line change
@@ -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 <Link to="/arbitrary">Navigate</Link>;
},
},
{
path: "/arbitrary",
Component: () => <h1>Arbitrary</h1>,
},
],
{ window: testWindow }
);

render(<RouterProvider router={router} />);
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 <Link to="/arbitrary">Navigate</Link>;
},
},
{
path: "/arbitrary",
Component: () => <h1>Arbitrary</h1>,
},
],
{ window: testWindow }
);

render(<RouterProvider router={router} />);
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 <Link to="/arbitrary">Navigate</Link>;
},
},
{
path: "/arbitrary",
Component: () => <h1>Arbitrary</h1>,
},
],
{ window: testWindow }
);

render(<RouterProvider router={router} />);
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(`<!DOCTYPE html>`, { url: "http://localhost/" });
dom.window.history.replaceState(null, "", (isHash ? "#" : "") + initialUrl);
return dom.window as unknown as Window;
}
14 changes: 7 additions & 7 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down

0 comments on commit 1b8ee16

Please sign in to comment.