-
-
Save codeBelt/8564fa4d9a5719708198b0cddadaca3b to your computer and use it in GitHub Desktop.
| import SingletonRouter, { Router } from 'next/router'; | |
| import { useEffect } from 'react'; | |
| const defaultConfirmationDialog = async (msg?: string) => window.confirm(msg); | |
| /** | |
| * Inspiration from: https://stackoverflow.com/a/70759912/2592233 | |
| */ | |
| export const useLeavePageConfirmation = ( | |
| shouldPreventLeaving: boolean, | |
| message: string = 'Changes you made may not be saved.', | |
| confirmationDialog: (msg?: string) => Promise<boolean> = defaultConfirmationDialog | |
| ) => { | |
| useEffect(() => { | |
| // @ts-ignore because "change" is private in Next.js | |
| if (!SingletonRouter.router?.change) { | |
| return; | |
| } | |
| // @ts-ignore because "change" is private in Next.js | |
| const originalChangeFunction = SingletonRouter.router.change; | |
| const originalOnBeforeUnloadFunction = window.onbeforeunload; | |
| /* | |
| * Modifying the window.onbeforeunload event stops the browser tab/window from | |
| * being closed or refreshed. Since it is not possible to alter the close or reload | |
| * alert message, an empty string is passed to trigger the alert and avoid confusion | |
| * about the option to modify the message. | |
| */ | |
| if (shouldPreventLeaving) { | |
| window.onbeforeunload = () => ''; | |
| } else { | |
| window.onbeforeunload = originalOnBeforeUnloadFunction; | |
| } | |
| /* | |
| * Overriding the router.change function blocks Next.js route navigations | |
| * and disables the browser's back and forward buttons. This opens up the | |
| * possibility to use the window.confirm alert instead. | |
| */ | |
| if (shouldPreventLeaving) { | |
| // @ts-ignore because "change" is private in Next.js | |
| SingletonRouter.router.change = async (...args) => { | |
| const [historyMethod, , as] = args; | |
| // @ts-ignore because "state" is private in Next.js | |
| const currentUrl = SingletonRouter.router?.state.asPath.split('?')[0]; | |
| const changedUrl = as.split('?')[0]; | |
| const hasNavigatedAwayFromPage = currentUrl !== changedUrl; | |
| const wasBackOrForwardBrowserButtonClicked = historyMethod === 'replaceState'; | |
| let confirmed = false; | |
| if (hasNavigatedAwayFromPage) { | |
| confirmed = await confirmationDialog(message); | |
| } | |
| if (confirmed) { | |
| // @ts-ignore because "change" is private in Next.js | |
| Router.prototype.change.apply(SingletonRouter.router, args); | |
| } else if (wasBackOrForwardBrowserButtonClicked && hasNavigatedAwayFromPage) { | |
| /* | |
| * The URL changes even if the user clicks "false" to navigate away from the page. | |
| * It is necessary to update it to reflect the current URL. | |
| */ | |
| // @ts-ignore because "state" is private in Next.js | |
| await SingletonRouter.router?.push(SingletonRouter.router?.state.asPath); | |
| /* | |
| * @todo | |
| * I attempted to determine if the user clicked the forward or back button on the browser, | |
| * but was unable to find a solution after several hours of effort. As a result, I temporarily | |
| * hardcoded it to assume the back button was clicked, since that is the most common scenario. | |
| * However, this may cause issues with the URL if the forward button is actually clicked. | |
| * I hope that a solution can be found in the future. | |
| */ | |
| const browserDirection = 'back'; | |
| browserDirection === 'back' | |
| ? history.go(1) // back button | |
| : history.go(-1); // forward button | |
| } | |
| }; | |
| } | |
| /* | |
| * When the component is unmounted, the original change function is assigned back. | |
| */ | |
| return () => { | |
| // @ts-ignore because "change" is private in Next.js | |
| SingletonRouter.router.change = originalChangeFunction; | |
| window.onbeforeunload = originalOnBeforeUnloadFunction; | |
| }; | |
| }, [shouldPreventLeaving, message, confirmationDialog]); | |
| }; |
Are you doing it without a promise? If so, how? If you have a better way please let me know :) My way feels so sketchy!
Hello,
great piece of code!
I have one question: is it possible to perform a custom action instead of showing the alert?
I am trying to use this code to close a modal when back is pressed, instead of navigating back.
If I pass a callback to the hook and call it at line #53, it is not executed but I still get the alert.
Hmm, I also have my own custom modal so maybe that is why I haven't noticed it breaking.
@kirkegaard Can you add a link your modal/promise code via a Gist or Repo? I would like to see how you did it.
Could you show me the example of the custom modal?
Check out this PR/branch to see how I make a custom dialog
Check out this PR/branch to see how I make a custom dialog
I did myself with promise and resolve using hooks and context. I think yours are similar but using a global store. However there're still two cases not solved properly. I've ported your code to my nextjs 13 page router, and confirmed with your example website too.
- the tab refresh and close still defaults to chrome native dialog, I've also exhausted a ton of time finding alternatives, but not found any.
- For the browser button nav back and forward, I see you push the state, but chrome doesn't respect that, the url doesn't change back to current, if cancelled out.
what about app router in next 13?? i can't use this for app router because router in next13 come from next/navigation
I threw this together real quick but something like this :)
https://codesandbox.io/p/sandbox/cocky-northcutt-vbww1k
I dont think its the best approach. It seems kind of hacky and doesnt handle if the user clicks the back button twice.