Description
Which project does this relate to?
Router
Describe the bug
There is a bug with the useBlocker
hook where under specific conditions, both a custom UI blocker AND the browser's native beforeunload
confirmation dialog appear in sequence. The custom UI blocker appears first, and then after calling proceed
, the browser's native confirmation dialog also appears.
The issue occurs when all of the following conditions are met:
withResolver
is set totrue
(to enable the custom UI)- Both
enableBeforeUnload
andshouldBlockFn
returntrue
until the hook is fully unmounted (in our case, based on form dirtiness detection) - In the
beforeLoad
of the destination route, there is athrow redirect
to an external site
Additionally, passing ignoreBlocker: true
to the redirect
function that is thrown does not prevent the browser confirmation dialog from showing.
Your Example Website or App
https://stackblitz.com/edit/vitejs-vite-hyxmxvkd?file=src%2Froutes%2Findex.tsx
Steps to Reproduce the Bug or Issue
-
Create a component that uses
useBlocker
with:// In src/routes/index.tsx const { status, proceed, reset } = useBlocker({ shouldBlockFn: () => true, enableBeforeUnload: () => true, withResolver: true, });
-
Add a route with a
beforeLoad
function that contains a redirect to an external site:// In src/routes/external.tsx export const Route = createFileRoute('/external')({ component: External, beforeLoad: () => { throw redirect({ href: 'https://example.com/' }); }, });
-
Try to navigate to the '/external' route (you can add a link to it on the index page)
Expected behavior
When navigating to a route that redirects externally, only the custom UI blocker should appear. Once the user confirms by clicking "proceed" in the custom UI, the navigation should continue without triggering the browser's native beforeunload confirmation dialog.
Screenshots or Videos
No response
Platform
- OS: macOS
- Browser: Chrome
- Version: 1.119.0
Additional context
I found a workaround by modifying the enableBeforeUnload
function to include a check against the router state:
enableBeforeUnload: () => formIsDirty && router.state.status !== "pending"
This prevents the browser confirmation dialog from appearing when the router status is "pending", which occurs during the redirect. However, this seems like a workaround for what appears to be a bug in the interaction between useBlocker
, beforeunload events, and redirects to external sites.
The key reproduction components are:
- A route using
useBlocker
with bothshouldBlockFn
andenableBeforeUnload
returning true - A separate route with a
beforeLoad
function that throws an external redirect - When navigating to the external route, first the custom UI blocking appears, and after clicking "proceed", a browser confirmation dialog also appears
It would be helpful if there was a more official way to handle this case, perhaps by having ignoreBlocker: true
properly bypass all forms of blocking, including the beforeunload event.