|
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]); |
|
}; |