Search code examples
reactjstypescriptreact-router-dom

Detect route change beforehand in SPA using react-router-dom v6 for routing


I am building SPA application. I have component with form changes to which could be either saved or canceled depending which button user pressed. I have a flag which indicates that some changes were made in this form.

I am using react-router-dom v6 for routing like this:

<Routes>
  <Route />
  <Route />
  <Route />
</Routes>

I want to show user a warning when he has unsaved changes and trying to navigate to another page (another route). It seems to me that there is no such a mechanic, I am also still not able to write a pure js script to solve this problem. beforeunload not working for me because it is only triggered on page refresh.

Is there any way I can achieve this?


Solution

  • This component accompanied by usage of the new createBrowserRouter react-router-dom api solved my problem. Anthough it is still not clear how to appropriately use it with query params, blocker seems not be working when I am changing them.

    import { useDidUpdate, useDisclosure } from '@mantine/hooks';
    import { memo, useCallback } from 'react';
    import { useTranslation } from 'react-i18next';
    import { unstable_BlockerFunction, useBlocker } from 'react-router-dom';
    import { WarningModal } from '../Modals/WarningModal/WarningModal';
    
    interface Props {
      shouldBlock: boolean | unstable_BlockerFunction;
    }
    
    const ChangesUnsavedBlocker = memo((props: Props) => {
      const { shouldBlock } = props;
    
      const { t } = useTranslation('common', {
        keyPrefix: 'changes_unsaved_blocker',
      });
    
      // https://reactrouter.com/en/6.21.1/hooks/use-blocker
      const blocker = useBlocker(shouldBlock);
    
      const [warningShown, { open: showWarning, close: hideWarning }] = useDisclosure(false);
    
      useDidUpdate(() => {
        if (blocker.state === 'blocked') {
          showWarning();
        } else {
          hideWarning();
        }
      }, [blocker.state]);
    
      const handleProceed = useCallback(() => {
        blocker.proceed?.();
    
        hideWarning();
      }, [blocker, hideWarning]);
    
      const handleReset = useCallback(() => {
        blocker.reset?.();
    
        hideWarning();
      }, [blocker, hideWarning]);
    
      return (
        <WarningModal
          width="416px"
          icon="warning"
          maxHeight="348px"
          title={t('title')}
          annotation={t('annotation')}
          approveTitle={t<string>('approve_title')}
          isOpened={warningShown}
          onClose={handleReset}
          onCancel={handleReset}
          onApprove={handleProceed}
        />
      );
    });
    
    export { ChangesUnsavedBlocker };