Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react-router-dom): Fix usePrompt invalid blocker state transition #10687

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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