-
-
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]); | |
}; |
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.
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.
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
This seems to break the browser navigation on IOS. It seems like
confirm
isnt "await'able"? Or maybe ios refuses to show aconfirm
when using the browser navigation?EDIT,
Ios DOES ignore confirm, alert and other methods when using the browser navigation. I was able to solve this by making my own confirm modal that wraps a promise and waits for the users confirmation.