-
-
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.