Skip to content

Instantly share code, notes, and snippets.

@codeBelt
Created May 23, 2023 02:17
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codeBelt/8564fa4d9a5719708198b0cddadaca3b to your computer and use it in GitHub Desktop.
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]);
};
@kirkegaard
Copy link

kirkegaard commented May 24, 2023

This seems to break the browser navigation on IOS. It seems like confirm isnt "await'able"? Or maybe ios refuses to show a confirm 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.

@codeBelt
Copy link
Author

codeBelt commented May 24, 2023

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.

@kirkegaard
Copy link

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.

@kirkegaard
Copy link

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!

@apperside
Copy link

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.

@ZehuaZhang
Copy link

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?

@codeBelt
Copy link
Author

Check out this PR/branch to see how I make a custom dialog

codeBelt/warn-unsaved-changes-leaving-web-page-nextjs#3

@ZehuaZhang
Copy link

ZehuaZhang commented Oct 23, 2023

Check out this PR/branch to see how I make a custom dialog

codeBelt/warn-unsaved-changes-leaving-web-page-nextjs#3

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.

@iamnevir
Copy link

iamnevir commented Nov 9, 2023

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