Skip to content

Commit cbda9cf

Browse files
authored
Fix issue with reused blockers on subsequent navigations (#10656)
1 parent 775bff9 commit cbda9cf

File tree

5 files changed

+87
-8
lines changed

5 files changed

+87
-8
lines changed

.changeset/fix-reused-blocker.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"react-router": patch
3+
"@remix-run/router": patch
4+
---
5+
6+
Fix issue with reused blockers on subsequent navigations

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
},
110110
"filesize": {
111111
"packages/router/dist/router.umd.min.js": {
112-
"none": "46.5 kB"
112+
"none": "46.6 kB"
113113
},
114114
"packages/react-router/dist/react-router.production.min.js": {
115115
"none": "13.8 kB"

packages/react-router-dom/__tests__/use-blocker-test.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,77 @@ describe("navigation blocking with useBlocker", () => {
158158
act(() => root.unmount());
159159
});
160160

161+
it("handles reused blocker in a layout route", async () => {
162+
router = createMemoryRouter([
163+
{
164+
Component() {
165+
let blocker = useBlocker(true);
166+
return (
167+
<div>
168+
<Link to="/one">/one</Link>
169+
<Link to="/two">/two</Link>
170+
<Outlet />
171+
<p>{blocker.state}</p>
172+
{blocker.state === "blocked" ? (
173+
<button onClick={() => blocker.proceed?.()}>Proceed</button>
174+
) : null}
175+
</div>
176+
);
177+
},
178+
children: [
179+
{
180+
path: "/",
181+
element: <h1>Home</h1>,
182+
},
183+
{
184+
path: "/one",
185+
element: <h1>One</h1>,
186+
},
187+
{
188+
path: "/two",
189+
element: <h1>Two</h1>,
190+
},
191+
],
192+
},
193+
]);
194+
195+
act(() => {
196+
root = ReactDOM.createRoot(node);
197+
root.render(<RouterProvider router={router} />);
198+
});
199+
200+
// Start on /
201+
expect(node.querySelector("h1")?.textContent).toBe("Home");
202+
expect(node.querySelector("p")?.textContent).toBe("unblocked");
203+
expect(node.querySelector("button")).toBeNull();
204+
205+
// Blocked navigation to /one
206+
act(() => click(node.querySelector("a[href='/one']")));
207+
expect(node.querySelector("h1")?.textContent).toBe("Home");
208+
expect(node.querySelector("p")?.textContent).toBe("blocked");
209+
expect(node.querySelector("button")?.textContent).toBe("Proceed");
210+
211+
// Proceed to /one
212+
act(() => click(node.querySelector("button")));
213+
expect(node.querySelector("h1")?.textContent).toBe("One");
214+
expect(node.querySelector("p")?.textContent).toBe("unblocked");
215+
expect(node.querySelector("button")).toBeNull();
216+
217+
// Blocked navigation to /two
218+
act(() => click(node.querySelector("a[href='/two']")));
219+
expect(node.querySelector("h1")?.textContent).toBe("One");
220+
expect(node.querySelector("p")?.textContent).toBe("blocked");
221+
expect(node.querySelector("button")?.textContent).toBe("Proceed");
222+
223+
// Proceed to /two
224+
act(() => click(node.querySelector("button")));
225+
expect(node.querySelector("h1")?.textContent).toBe("Two");
226+
expect(node.querySelector("p")?.textContent).toBe("unblocked");
227+
expect(node.querySelector("button")).toBeNull();
228+
229+
act(() => root.unmount());
230+
});
231+
161232
describe("on <Link> navigation", () => {
162233
describe("blocker returns false", () => {
163234
beforeEach(() => {

packages/react-router/lib/hooks.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,6 @@ export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker {
939939
let state = useDataRouterState(DataRouterStateHook.UseBlocker);
940940

941941
let [blockerKey, setBlockerKey] = React.useState("");
942-
let [blocker, setBlocker] = React.useState<Blocker>(IDLE_BLOCKER);
943942
let blockerFunction = React.useCallback<BlockerFunction>(
944943
(arg) => {
945944
if (typeof shouldBlock !== "function") {
@@ -986,15 +985,15 @@ export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker {
986985
// key of "". Until then we just have the IDLE_BLOCKER.
987986
React.useEffect(() => {
988987
if (blockerKey !== "") {
989-
setBlocker(router.getBlocker(blockerKey, blockerFunction));
988+
router.getBlocker(blockerKey, blockerFunction);
990989
}
991990
}, [router, blockerKey, blockerFunction]);
992991

993-
// Prefer the blocker from state since DataRouterContext is memoized so this
994-
// ensures we update on blocker state updates
992+
// Prefer the blocker from `state` not `router.state` since DataRouterContext
993+
// is memoized so this ensures we update on blocker state updates
995994
return blockerKey && state.blockers.has(blockerKey)
996995
? state.blockers.get(blockerKey)!
997-
: blocker;
996+
: IDLE_BLOCKER;
998997
}
999998

1000999
/**

packages/router/router.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,8 +1026,11 @@ export function createRouter(init: RouterInit): Router {
10261026

10271027
// On a successful navigation we can assume we got through all blockers
10281028
// so we can start fresh
1029-
let blockers = new Map();
1030-
blockerFunctions.clear();
1029+
let blockers = state.blockers;
1030+
if (blockers.size > 0) {
1031+
blockers = new Map(blockers);
1032+
blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));
1033+
}
10311034

10321035
// Always respect the user flag. Otherwise don't reset on mutation
10331036
// submission navigations unless they redirect

0 commit comments

Comments
 (0)