,
+ },
+ ],
+ },
+ ]);
+
+ act(() => {
+ root = ReactDOM.createRoot(node);
+ root.render();
+ });
+
+ expect(node.querySelector("h1")?.textContent).toBe("Home");
+
+ act(() => click(node.querySelector("a[href='/about']")));
+ expect(node.querySelector("h1")?.textContent).toBe("About");
+
+ act(() => root.unmount());
+ });
+
describe("on navigation", () => {
describe("blocker returns false", () => {
beforeEach(() => {
diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx
index 0135ea0bcc..c3177d8645 100644
--- a/packages/react-router/lib/hooks.tsx
+++ b/packages/react-router/lib/hooks.tsx
@@ -972,12 +972,23 @@ export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker {
[basename, shouldBlock]
);
+ // This effect is in charge of blocker key assignment and deletion (which is
+ // tightly coupled to the key)
React.useEffect(() => {
let key = String(++blockerId);
- setBlocker(router.getBlocker(key, blockerFunction));
setBlockerKey(key);
return () => router.deleteBlocker(key);
- }, [router, setBlocker, setBlockerKey, blockerFunction]);
+ }, [router]);
+
+ // This effect handles assigning the blockerFunction. This is to handle
+ // unstable blocker function identities, and happens only after the prior
+ // effect so we don't get an orphaned blockerFunction in the router with a
+ // key of "". Until then we just have the IDLE_BLOCKER.
+ React.useEffect(() => {
+ if (blockerKey !== "") {
+ setBlocker(router.getBlocker(blockerKey, blockerFunction));
+ }
+ }, [router, blockerKey, blockerFunction]);
// Prefer the blocker from state since DataRouterContext is memoized so this
// ensures we update on blocker state updates