usePreventNavigationOnUnsavedChanges.ts 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { useRouter } from 'next/router'
  2. import { useEffect, useMemo, useState } from 'react'
  3. import { useStaticEffectEvent } from '../useStaticEffectEvent'
  4. import { BASE_PATH } from '@/lib/constants'
  5. interface UsePreventNavigationOnUnsavedChangesOptions {
  6. /*
  7. * Boolean indicating whether there are changes that would be lost if users navigate to another
  8. * page or close the browser tab
  9. */
  10. hasChanges: boolean
  11. }
  12. interface UsePreventNavigationOnUnsavedChangesReturn {
  13. /*
  14. * Cancel the navigation and keep the changes
  15. */
  16. handleCancel: () => void
  17. /*
  18. * Confirm the navigation and lose the changes
  19. */
  20. handleConfirm: () => void
  21. /*
  22. * Boolean indicating whether UI to request users confirmation for the navigation should be
  23. * displayed
  24. */
  25. shouldConfirm: boolean
  26. }
  27. /*
  28. * Hook that prevents navigation when users could lose their changes.
  29. * It prevents both NextJS and browser navigation (such as when closing the tab)
  30. */
  31. export const usePreventNavigationOnUnsavedChanges = ({
  32. hasChanges,
  33. }: UsePreventNavigationOnUnsavedChangesOptions): UsePreventNavigationOnUnsavedChangesReturn => {
  34. const router = useRouter()
  35. const [navigateUrl, setNavigateUrl] = useState<string>()
  36. const [confirmNavigate, setConfirmNavigate] = useState(false)
  37. useEffect(() => {
  38. const handleBeforeUnload = (e: BeforeUnloadEvent) => {
  39. if (hasChanges) {
  40. e.preventDefault()
  41. e.returnValue = '' // deprecated, but older browsers still require this
  42. }
  43. }
  44. const handleBrowseAway = (url: string) => {
  45. if (hasChanges && !confirmNavigate) {
  46. setNavigateUrl(url)
  47. throw 'Route change declined' // Just to prevent the route change
  48. return
  49. }
  50. setNavigateUrl(undefined)
  51. }
  52. window.addEventListener('beforeunload', handleBeforeUnload)
  53. router.events.on('routeChangeStart', handleBrowseAway)
  54. return () => {
  55. window.removeEventListener('beforeunload', handleBeforeUnload)
  56. router.events.off('routeChangeStart', handleBrowseAway)
  57. }
  58. // eslint-disable-next-line react-hooks/exhaustive-deps
  59. }, [confirmNavigate, hasChanges])
  60. const handleCancel = useStaticEffectEvent(() => {
  61. setNavigateUrl(undefined)
  62. })
  63. const handleConfirm = useStaticEffectEvent(() => {
  64. setConfirmNavigate(true)
  65. let urlToNavigate = navigateUrl ?? '/'
  66. if (BASE_PATH && urlToNavigate.startsWith(BASE_PATH)) {
  67. urlToNavigate = urlToNavigate.slice(BASE_PATH.length) || '/'
  68. }
  69. if (!urlToNavigate.startsWith('/')) urlToNavigate = `/${urlToNavigate}`
  70. setNavigateUrl(undefined)
  71. router.push(urlToNavigate)
  72. })
  73. return useMemo(
  74. () => ({
  75. handleCancel,
  76. handleConfirm,
  77. shouldConfirm: !!navigateUrl,
  78. }),
  79. [navigateUrl, handleCancel, handleConfirm]
  80. )
  81. }