Skip to content

Instantly share code, notes, and snippets.

@calvinf
Created December 27, 2022 21:28
Show Gist options
  • Save calvinf/d660d4050aba3e2f555180379a1a08ee to your computer and use it in GitHub Desktop.
Save calvinf/d660d4050aba3e2f555180379a1a08ee to your computer and use it in GitHub Desktop.
Next.js Page Navigation Lock
import { useEffect } from 'react';
import { useRouter } from 'next/router';
// helpful issues/posts when building this
// https://github.com/vercel/next.js/issues/2694#issuecomment-732990201
// https://github.com/vercel/next.js/issues/2476#issuecomment-850030407
// https://github.com/vercel/next.js/discussions/32231#discussioncomment-1766752
// https://github.com/vercel/next.js/issues/2476#issuecomment-843311387
/**
* Ask for confirmation before navigating by link or closing browser tab/window.
*
* Note: this should only be used once for a given page. Multiple instances
* of this on a single page could cause conflicts.
*
* @param isEnabled whether the navigation lock is enabled
* @param message optional message to show when navigation lock enabled
*/
export const useNavigationLock = (
isEnabled: boolean = true,
message: string = 'You have unsaved changes. Do you wish to leave this page?',
) => {
const router = useRouter();
useEffect(() => {
let isWarned = false;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isEnabled && !isWarned) {
const event = e || window.event;
event.returnValue = message;
return message;
}
return null;
};
const handleRouteChange = (url: string) => {
if (router.asPath !== url && isEnabled && !isWarned) {
isWarned = true;
if (window.confirm(message)) {
router.push(url);
} else {
isWarned = false;
router.events.emit('routeChangeError');
// Normally for proper error handling, we would
// `throw new Error('message goes here')`. But in this case,
// throwing a string avoids showing the Error screen in
// Next.js in development.
//
// We may need to set Honeybadger to ignore this error.
throw 'NavigationLock: route change blocked. Please ignore 💯.';
}
}
};
// turn on the event handlers for route change and unload
router.events.on('routeChangeStart', handleRouteChange);
window.addEventListener('beforeunload', handleBeforeUnload);
// This differs from other examples from the Next.js
// discussions because we cannot reliably control the route when
// user goes forward and back (with Next 12 or 13).
router.beforePopState(({ url }) => {
if (router.asPath !== url && isEnabled && !isWarned) {
// we set `isWarned` to `true` to skip the modal showing in handleRouteChange.
isWarned = true;
}
// We return true to allow the pop state behavior to continue as expected.
return true;
});
// when the hook is unmounted by changing to another view,
// we remove or turn off the event listeners
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
router.events.off('routeChangeStart', handleRouteChange);
// We don't have a way to unset beforePopState so instead we
// make sure we're always allowing popstate to continue as expected
router.beforePopState(() => true);
};
}, [isEnabled, message, router]);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment