skip to Main Content

In my NextJs app I have a page in which I render a modal that is open when a query parameter is set and closes when that query parameter is not present.

Since in the modal there will be a form, I want to alert the user if they try to navigate away clicking the browser’s back arrow without saving, but since the only change in the path is a query parameter that gets removed I am having trouble preventing the user from navigating away.

Here is a simplified example of my code with the closest I got to:

import {
  Box,
  Button,
  Modal,
  ModalBody,
  ModalContent,
  ModalOverlay,
  ModalProps,
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';

const ItemDetails = (props: Omit<ModalProps, 'children'>) => {
  return (
    <Modal {...props}>
      <ModalOverlay />
      <ModalBody>
        <ModalContent>Hello I am an item</ModalContent>
      </ModalBody>
    </Modal>
  );
};

const Page = () => {
  const router = useRouter();
  const { isItemDetailsOpen } = router.query;

  const isOpen = isItemDetailsOpen === 'true';

  const onClose = () => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { isItemDetailsOpen, ...restOfQuery } = router.query;
    router.query = restOfQuery;
    router.push(router);
  };

  useEffect(() => {
    const handleRouteChange = () => {
      if (isOpen) {
        const confirmLeave = window.confirm(
          'You are currently editing. Are you sure you want to leave and lose your changes?'
        );
        if (!confirmLeave) {
          router.events.emit('routeChangeError');
          throw 'Abort route change. Please ignore this error.';
        }
      }
    };

    router.events.on('routeChangeStart', handleRouteChange);

    return () => {
      router.events.off('routeChangeStart', handleRouteChange);
    };
  }, [router, isOpen]);

  return (
    <Box>
      This is a Page
      <Button
        onClick={() =>
          router.push({
            ...router,
            query: { ...router.query, isItemDetailsOpen: 'true' },
          })
        }
      >
        Open Item Details
      </Button>
      <ItemDetails isOpen={isOpen} onClose={onClose} />
    </Box>
  );
};

export default Page;

When I open the modal and then hit back on the browser’s arrow I do get the window.confirm warning, but the url already changed even if I didn’t confirm.

Is there a way to prevent query params to change and to only change when the user confirms?

Thanks all!

2

Answers


  1. Unfortunately, Next.js doesn’t provide a built-in way to block route changes directly. This is particularly tricky with query parameters changes because they don’t trigger the typical "beforeunload" event used for page navigations.

    A possible workaround approach involves managing a state that tracks whether the user has pending changes and then using a confirmation dialog to warn the user. You already have this with the window.confirm in your routeChangeStart event, but as you noticed, it doesn’t prevent the URL change.

    One way to handle the URL changing issue is to manually manage the state and not rely on the query params for showing the modal. This means, instead of toggling isItemDetailsOpen in the query params, you may have to manage this state internally within the component or context.

    Login or Signup to reply.
  2. You can accomplish this by using the beforeunload event to handle the confirmation dialog.

    Here’s how you can modify your code to implement this:

    import {
      Box,
      Button,
      Modal,
      ModalBody,
      ModalContent,
      ModalOverlay,
      ModalProps,
    } from '@chakra-ui/react';
    import { useRouter } from 'next/router';
    import { useEffect, useState } from 'react';
    
    const ItemDetails = (props: Omit<ModalProps, 'children'>) => {
      return (
        <Modal {...props}>
          <ModalOverlay />
          <ModalBody>
            <ModalContent>Hello I am an item</ModalContent>
          </ModalBody>
        </Modal>
      );
    };
    
    const Page = () => {
      const router = useRouter();
      const { isItemDetailsOpen } = router.query;
    
      const isOpen = isItemDetailsOpen === 'true';
      const [confirmLeave, setConfirmLeave] = useState(false);
    
      const onClose = () => {
        setConfirmLeave(false);
        router.replace(router.pathname); // Reset the query parameters
      };
    
      useEffect(() => {
        const handleRouteChange = (url: string) => {
          if (isOpen && !confirmLeave) {
            const leaveConfirmation = window.confirm(
              'You are currently editing. Are you sure you want to leave and lose your changes?'
            );
            if (!leaveConfirmation) {
              setConfirmLeave(true);
              throw 'Abort route change. Please ignore this error.';
            }
          }
        };
    
        const handleBeforeUnload = (event: BeforeUnloadEvent) => {
          if (isOpen && !confirmLeave) {
            event.preventDefault();
            event.returnValue = ''; // Needed for Chrome
            return (
              'You are currently editing. Are you sure you want to leave and lose your changes?'
            );
          }
        };
    
        router.events.on('routeChangeStart', handleRouteChange);
        window.addEventListener('beforeunload', handleBeforeUnload);
    
        return () => {
          router.events.off('routeChangeStart', handleRouteChange);
          window.removeEventListener('beforeunload', handleBeforeUnload);
        };
      }, [router, isOpen, confirmLeave]);
    
      return (
        <Box>
          This is a Page
          <Button
            onClick={() =>
              router.push({
                ...router,
                query: { ...router.query, isItemDetailsOpen: 'true' },
              })
            }
          >
            Open Item Details
          </Button>
          <ItemDetails isOpen={isOpen} onClose={onClose} />
        </Box>
      );
    };
    
    export default Page;
    

    In this updated code:

    • We introduced a confirmLeave state variable to track whether the user has confirmed leaving the page.
    • We reset the query parameters using router.replace(router.pathname) in the onClose function instead of directly manipulating router.query.
    • We added a beforeunload event listener to handle the confirmation dialog when the user tries to leave the page. This event fires when the user attempts to leave the page by any means (closing the tab, refreshing, navigating away, etc.).
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search