Skip to content

Instantly share code, notes, and snippets.

@rmorse
Last active December 21, 2023 04:22
Show Gist options
  • Star 57 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save rmorse/426ffcc579922a82749934826fa9f743 to your computer and use it in GitHub Desktop.
Save rmorse/426ffcc579922a82749934826fa9f743 to your computer and use it in GitHub Desktop.
Adds back in `useBlocker` and `usePrompt` to `react-router-dom` version 6.0.2 (they removed after the 6.0.0 beta, temporarily)
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* @param blocker
* @param when
* @see https://reactrouter.com/api/useBlocker
*/
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}
const MyComponent = () => {
const formIsDirty = true; // Condition to trigger the prompt.
usePrompt( 'Leave screen?', formIsDirty );
return (
<div>Hello world</div>
);
};
@jsmcconkey
Copy link

@hackerghost93 - I'm sure you got this sorted out by now, but I used patch package myself. If you install patch-package you can make the fix. Then you can run the patch-package tool. Then whenever your modules are installed the patch will then be applied. you can read more about his package at the following link.

https://www.npmjs.com/package/patch-package

@leminhhung-NamiQ
Copy link

it's not the best way to do, who else has another way, of change node modules it will fail, please help, thanks

@jsmcconkey
Copy link

@leminhhung-NamiQ that is highly dependent on the type of change that you do to the node module and the context of that change.

From my point of view this is a small change to a type interface applied to a specific version of a the react-router module. Yes, at some point in the future this patch might fail, however, the risk is minimal. The way I see it at the moment is you can:

  1. Update the type interface through patching the package.
  2. Ignore the type error for now.
  3. Fork the repo and make the change yourself.

If you plan to fork the repo you might as well patch the package. If you lock your node module to a specific version and then watch for updates to react router you should be fine. Based upon this comment it would appear the intention is to bring "block" back.

remix-run/react-router#8139 (comment)

@julix-unity
Copy link

Can't you just extend the type and use as?

@matrey
Copy link

matrey commented May 2, 2022

There is a working Typescript version here: https://stackoverflow.com/a/71587163/8046487

import { History, Transition } from 'history';
  • tx is a Transition
  • as for navigator:
type ExtendNavigator = Navigator & Pick<History, "block">;
[...]
const unblock = (navigator as any as ExtendNavigator).block(...

@zwilderrr
Copy link

What's the issue with this approach? Meaning, why isn't this a part of v6?

@Ralfarios
Copy link

I do exactly the same like this code, but I still moved to other routes even cancel button clicked. :/

@mknospe
Copy link

mknospe commented Aug 4, 2022

Works like a charm. Thanks for sharing! 💯

@borstelmannl
Copy link

if i use this workaround: https://stackoverflow.com/a/71587163/8046487
i receive an exception: navigator.block is not a function. Anyone else having this issue?

i am using the newest react router version 6.4

@matrey
Copy link

matrey commented Sep 19, 2022

You can check the original github issue that led to the creation of this gist: remix-run/react-router#8139 (comment)

Just fyi, it's still possible to use navigator.block via history package with v6.4.0, by replacing BrowserRouter with unstable_HistoryRouter

@fcmdvtk4r
Copy link

Resetting any of the dependencies at line 38 when canceling the navigation solved the TODO for me.

Thanks for sharing!

@alexrabin
Copy link

if i use this workaround: https://stackoverflow.com/a/71587163/8046487 i receive an exception: navigator.block is not a function. Anyone else having this issue?

i am using the newest react router version 6.4

@borstelmannl I am also having this issue on 6.4

@borstelmannl
Copy link

hey @alexrabin,
just replace your Browserrouter or any other router you have with the unstable_HistoryRouter. Then the example should work perfectly fine.

@bufke
Copy link

bufke commented Oct 14, 2022

Is there a way to use unstable_HistoryRouter with createBrowserRouter? The current react router tutorial uses createBrowserRouter.

@yashsharma04
Copy link

yashsharma04 commented Dec 21, 2022

@bufke did you find a way to solve this with createBrowserRouter ?

@bufke
Copy link

bufke commented Jan 16, 2023

I did not, I ended up using <BrowserRouter>.

@rmorse
Copy link
Author

rmorse commented Jan 22, 2023

This approach may not be needed soon as we now have (as of v6.7.0) unstable_useBlocker and unstable_usePrompt:
remix-run/react-router#8139 (comment)

Waiting for the docs to be written before I update this gist.

@ngocducdim
Copy link

Not woking with some gaming mouse have back button, any ideals?

@mgrahamjo
Copy link

Here's a useBeforeUnload hook for v6.4.3 that catches both react router navigation and browser navigation:

import { useCallback } from 'react';
import { useBeforeUnload as _useBeforeUnload, unstable_usePrompt as usePrompt } from 'react-router-dom';

export default function useBeforeUnload(doBlock?: boolean) {
  _useBeforeUnload(
    useCallback(e => {
      if (doBlock) {
        e.preventDefault();
        return e.returnValue = '';
      }
    }, [doBlock])
  );

  usePrompt({
    when: doBlock,
    message: 'Discard unsaved changes?'
  });
}

@Noah-JJ
Copy link

Noah-JJ commented Mar 13, 2023

thx!

@ikapta
Copy link

ikapta commented Mar 30, 2023

Here's a useBeforeUnload hook for v6.4.3 that catches both react router navigation and browser navigation:

import { useCallback } from 'react';
import { useBeforeUnload as _useBeforeUnload, unstable_usePrompt as usePrompt } from 'react-router-dom';

export default function useBeforeUnload(doBlock?: boolean) {
  _useBeforeUnload(
    useCallback(e => {
      if (doBlock) {
        e.preventDefault();
        return e.returnValue = '';
      }
    }, [doBlock])
  );

  usePrompt({
    when: doBlock,
    message: 'Discard unsaved changes?'
  });
}

This is awesome, thanks for sharing! 💯

@LongTCH
Copy link

LongTCH commented Jul 9, 2023

you can simply add these:

useEffect(() => {
   window.addEventListener("beforeunload", onBeforeUnload);
   return () => {
     window.removeEventListener("beforeunload", onBeforeUnload);
   };
 });

 const onBeforeUnload = (e) => {
   if (hasDirty) {
     e.preventDefault();
     e.returnValue = "";
   }
 };

Reference : https://claritydev.net/blog/display-warning-for-unsaved-form-data-on-page-exit

@benlieb
Copy link

benlieb commented Aug 28, 2023

you can simply add these:

useEffect(() => {
   window.addEventListener("beforeunload", onBeforeUnload);
   return () => {
     window.removeEventListener("beforeunload", onBeforeUnload);
   };
 });

 const onBeforeUnload = (e) => {
   if (hasDirty) {
     e.preventDefault();
     e.returnValue = "";
   }
 };

Reference : https://claritydev.net/blog/display-warning-for-unsaved-form-data-on-page-exit

This doesn't fire during route transitions...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment