diff --git a/.changeset/brave-shrimps-wonder.md b/.changeset/brave-shrimps-wonder.md new file mode 100644 index 0000000000..12f03e97be --- /dev/null +++ b/.changeset/brave-shrimps-wonder.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix useBlocker when blocking function is quick to proceed diff --git a/contributors.yml b/contributors.yml index ea24af9bbd..5d4195ba00 100644 --- a/contributors.yml +++ b/contributors.yml @@ -26,6 +26,7 @@ - Armanio - arnassavickas - aroyan +- Artur- - ashusnapx - avipatel97 - awreese diff --git a/packages/react-router/__tests__/router/navigation-blocking-test.ts b/packages/react-router/__tests__/router/navigation-blocking-test.ts index 7f7a603e38..1764ee9faf 100644 --- a/packages/react-router/__tests__/router/navigation-blocking-test.ts +++ b/packages/react-router/__tests__/router/navigation-blocking-test.ts @@ -1,4 +1,5 @@ -import { createMemoryHistory } from "../../lib/router/history"; +import { waitFor } from "@testing-library/react"; +import { createBrowserHistory, createMemoryHistory } from "../../lib/router/history"; import type { Router } from "../../lib/router/router"; import { createRouter } from "../../lib/router/router"; @@ -461,6 +462,41 @@ describe("navigation blocking", () => { }); }); + describe("proceeds from blocked state using browser history", () => { + let fn = () => true; + + // we want to navigate so that `/about` is the previous entry in the + // stack here since it has a loader that won't resolve immediately + beforeEach(async () => { + const history = createBrowserHistory(); + + router = createRouter({ + history, + routes, + }); + + router.initialize(); + + await router.navigate("/"); + await router.navigate("/about"); + await router.navigate("/contact"); + }); + + it("proceeds after quick block of back navigation", async () => { + router.getBlocker("KEY", fn); + + await router.navigate(-1); // This does not really wait for the navigation to happen + await waitFor( + () => expect(router.getBlocker("KEY", fn).state).toBe("blocked"), + { interval: 1 } + ); // This awaits the navigation + router.getBlocker("KEY", fn).proceed!(); + await waitFor(() => + expect(router.state.location.pathname).toBe("/about") + ); + }); + }); + describe("resets from blocked state", () => { let fn = () => true; it("gets an 'unblocked' blocker after resetting navigation", async () => { diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 4b983f6265..19e2df632a 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1022,7 +1022,7 @@ export function createRouter(init: RouterInit): Router { // Flag to ignore the next history update, so we can revert the URL change on // a POP navigation that was blocked by the user without touching router state - let ignoreNextHistoryUpdate = false; + let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined; let pendingRevalidationDfd: ReturnType> | null = null; @@ -1037,8 +1037,9 @@ export function createRouter(init: RouterInit): Router { ({ action: historyAction, location, delta }) => { // Ignore this event if it was just us resetting the URL from a // blocked POP navigation - if (ignoreNextHistoryUpdate) { - ignoreNextHistoryUpdate = false; + if (unblockBlockerHistoryUpdate) { + unblockBlockerHistoryUpdate(); + unblockBlockerHistoryUpdate = undefined; return; } @@ -1060,7 +1061,9 @@ export function createRouter(init: RouterInit): Router { if (blockerKey && delta != null) { // Restore the URL to match the current UI, but don't update router state - ignoreNextHistoryUpdate = true; + const nextHistoryUpdatePromise = new Promise( + (resolve) => (unblockBlockerHistoryUpdate = resolve) + ); init.history.go(delta * -1); // Put the blocker into a blocked state @@ -1074,8 +1077,8 @@ export function createRouter(init: RouterInit): Router { reset: undefined, location, }); - // Re-do the same POP navigation we just blocked - init.history.go(delta); + // Re-do the same POP navigation we just blocked, after the url restoration is also complete + nextHistoryUpdatePromise.then(() => init.history.go(delta)); }, reset() { let blockers = new Map(state.blockers);