router icon indicating copy to clipboard operation
router copied to clipboard

`beforeunload` dialog triggered by redirect to external site even after original blocker proceed occurs

Open dawsonbooth opened this issue 8 months ago • 2 comments

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:

  1. withResolver is set to true (to enable the custom UI)
  2. Both enableBeforeUnload and shouldBlockFn return true until the hook is fully unmounted (in our case, based on form dirtiness detection)
  3. In the beforeLoad of the destination route, there is a throw 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

  1. Create a component that uses useBlocker with:

    // In src/routes/index.tsx
    const { status, proceed, reset } = useBlocker({
      shouldBlockFn: () => true,
      enableBeforeUnload: () => true,
      withResolver: true,
    });
    
  2. 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/' });
      },
    });
    
  3. 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:

  1. A route using useBlocker with both shouldBlockFn and enableBeforeUnload returning true
  2. A separate route with a beforeLoad function that throws an external redirect
  3. 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.

dawsonbooth avatar May 05 '25 21:05 dawsonbooth

@dawsonbooth this might have been resolved by #4917 - can you check if it's still an issue? there'll be a new release at the end of the CI run.

birkskyum avatar Nov 23 '25 00:11 birkskyum

@birkskyum This seems to still be an issue in v1.139.3 (latest) – I've updated the deps in the linked example for an up-to-date repro.

dawsonbooth avatar Nov 24 '25 18:11 dawsonbooth