Created
May 23, 2023 02:17
-
-
Save codeBelt/8564fa4d9a5719708198b0cddadaca3b to your computer and use it in GitHub Desktop.
TypeScript version of https://betterprogramming.pub/prevent-route-changes-and-unsaved-data-loss-in-next-js-f93622d73791
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | |
}; |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Check out this PR/branch to see how I make a custom dialog
codeBelt/warn-unsaved-changes-leaving-web-page-nextjs#3