diff --git a/.changeset/serious-news-kick.md b/.changeset/serious-news-kick.md new file mode 100644 index 0000000000..0aca964802 --- /dev/null +++ b/.changeset/serious-news-kick.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": patch +--- + +Fix blocker usage when `blocker.proceed` is called quickly/syncronously diff --git a/contributors.yml b/contributors.yml index 822592936a..05cda76d64 100644 --- a/contributors.yml +++ b/contributors.yml @@ -26,6 +26,7 @@ - Armanio - arnassavickas - aroyan +- Artur- - ashusnapx - avipatel97 - awreese diff --git a/packages/react-router-dom/__tests__/use-blocker-test.tsx b/packages/react-router-dom/__tests__/use-blocker-test.tsx index e0e6da73c4..e2fd57717a 100644 --- a/packages/react-router-dom/__tests__/use-blocker-test.tsx +++ b/packages/react-router-dom/__tests__/use-blocker-test.tsx @@ -15,10 +15,10 @@ import { type Router = ReturnType; -const LOADER_LATENCY_MS = 100; +const LOADER_LATENCY_MS = 200; async function slowLoader() { - await sleep(LOADER_LATENCY_MS); + await sleep(LOADER_LATENCY_MS / 2); return json(null); } @@ -1084,14 +1084,23 @@ describe("navigation blocking with useBlocker", () => { act(() => { click(node.querySelector("[data-action='back']")); }); - act(() => { + expect(node.innerHTML).toContain("

Contact

"); + await act(async () => { click(node.querySelector("[data-action='proceed']")); + expect([...router.state.blockers.values()][0]).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + await sleep(LOADER_LATENCY_MS); }); + expect(node.innerHTML).toContain("

About

"); expect(blocker).toEqual({ - state: "proceeding", + state: "unblocked", proceed: undefined, reset: undefined, - location: expect.any(Object), + location: undefined, }); }); diff --git a/packages/router/__tests__/navigation-blocking-test.ts b/packages/router/__tests__/navigation-blocking-test.ts index 5b010f8a1f..22494ccaeb 100644 --- a/packages/router/__tests__/navigation-blocking-test.ts +++ b/packages/router/__tests__/navigation-blocking-test.ts @@ -442,7 +442,7 @@ describe("navigation blocking", () => { router.getBlocker("KEY", fn); await router.navigate(-1); router.getBlocker("KEY", fn).proceed?.(); - await sleep(LOADER_LATENCY_MS); + await sleep(LOADER_LATENCY_MS + 10); expect(router.getBlocker("KEY", fn)).toEqual({ state: "unblocked", proceed: undefined, @@ -455,7 +455,7 @@ describe("navigation blocking", () => { router.getBlocker("KEY", fn); await router.navigate(-1); router.getBlocker("KEY", fn).proceed?.(); - await sleep(LOADER_LATENCY_MS); + await sleep(LOADER_LATENCY_MS + 10); expect(router.state.location.pathname).toBe("/about"); }); }); diff --git a/packages/router/router.ts b/packages/router/router.ts index 7d9c7f7e27..144be6e69d 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -1033,7 +1033,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; // Initialize the router, all side effects should be kicked off from here. // Implemented as a Fluent API for ease of: @@ -1045,8 +1045,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; } @@ -1068,7 +1069,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; + let nextHistoryUpdatePromise = new Promise((resolve) => { + unblockBlockerHistoryUpdate = resolve; + }); init.history.go(delta * -1); // Put the blocker into a blocked state @@ -1082,8 +1085,10 @@ 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. See: + // https://github.com/remix-run/react-router/issues/11613 + nextHistoryUpdatePromise.then(() => init.history.go(delta)); }, reset() { let blockers = new Map(state.blockers);