Skip to content

Commit 0ffa3dc

Browse files
authored
Fix loop in unstable_useBlocker when used with an unstable blocker function (#10652)
* Fix loop in unstable_useBlocker when used with an unstable blocker function * Remove getBlocker from key generation
1 parent 6c23794 commit 0ffa3dc

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

.changeset/fix-blocker-loop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fix loop in `unstable_useBlocker` when used with an unstable blocker function

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,50 @@ describe("navigation blocking with useBlocker", () => {
114114
act(() => root.unmount());
115115
});
116116

117+
it("handles unstable blocker function identities", async () => {
118+
let count = 0;
119+
router = createMemoryRouter([
120+
{
121+
element: React.createElement(() => {
122+
// New function identity on each render
123+
let b = useBlocker(() => false);
124+
blocker = b;
125+
if (++count > 50) {
126+
throw new Error("useBlocker caused a re-render loop!");
127+
}
128+
return (
129+
<div>
130+
<Link to="/about">/about</Link>
131+
<Outlet />
132+
</div>
133+
);
134+
}),
135+
children: [
136+
{
137+
path: "/",
138+
element: <h1>Home</h1>,
139+
},
140+
{
141+
path: "/about",
142+
element: <h1>About</h1>,
143+
},
144+
],
145+
},
146+
]);
147+
148+
act(() => {
149+
root = ReactDOM.createRoot(node);
150+
root.render(<RouterProvider router={router} />);
151+
});
152+
153+
expect(node.querySelector("h1")?.textContent).toBe("Home");
154+
155+
act(() => click(node.querySelector("a[href='/about']")));
156+
expect(node.querySelector("h1")?.textContent).toBe("About");
157+
158+
act(() => root.unmount());
159+
});
160+
117161
describe("on <Link> navigation", () => {
118162
describe("blocker returns false", () => {
119163
beforeEach(() => {

packages/react-router/lib/hooks.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -972,12 +972,23 @@ export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker {
972972
[basename, shouldBlock]
973973
);
974974

975+
// This effect is in charge of blocker key assignment and deletion (which is
976+
// tightly coupled to the key)
975977
React.useEffect(() => {
976978
let key = String(++blockerId);
977-
setBlocker(router.getBlocker(key, blockerFunction));
978979
setBlockerKey(key);
979980
return () => router.deleteBlocker(key);
980-
}, [router, setBlocker, setBlockerKey, blockerFunction]);
981+
}, [router]);
982+
983+
// This effect handles assigning the blockerFunction. This is to handle
984+
// unstable blocker function identities, and happens only after the prior
985+
// effect so we don't get an orphaned blockerFunction in the router with a
986+
// key of "". Until then we just have the IDLE_BLOCKER.
987+
React.useEffect(() => {
988+
if (blockerKey !== "") {
989+
setBlocker(router.getBlocker(blockerKey, blockerFunction));
990+
}
991+
}, [router, blockerKey, blockerFunction]);
981992

982993
// Prefer the blocker from state since DataRouterContext is memoized so this
983994
// ensures we update on blocker state updates

0 commit comments

Comments
 (0)