Skip to content

Instantly share code, notes, and snippets.

@codeBelt
Created May 23, 2023 02:17
Show Gist options
  • Select an option

  • Save codeBelt/8564fa4d9a5719708198b0cddadaca3b to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown

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
Copy Markdown
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
Copy Markdown

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
Copy Markdown

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
Copy Markdown

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
Copy Markdown

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
Copy Markdown
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
Copy Markdown

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
Copy Markdown

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