Search code examples
reactjsreact-router

How to prevent route change using react-router


There's a certain page in my React app that I would like to prevent the user from leaving if the form is dirty.

In my react-routes, I am using the onLeave prop like this:

<Route path="dependent" component={DependentDetails} onLeave={checkForm}/>

And my onLeave is:

const checkForm = (nextState, replace, cb) => {
  if (form.IsDirty) {
    console.log('Leaving so soon?');
    // I would like to stay on the same page somehow...
  }
};

Is there a way to prevent the new route from firing and keep the user on the same page?


Solution

  • Solved using useBlocker

    React router has a hook called useBlocker You can use it to prevent navigating away from a page.

    You can use the following hook to show a confirm dialog with cancel and confirm callbacks:

    import { useCallback, useEffect, useState } from 'react';
    import { useBlocker, useNavigate } from 'react-router-dom';
    
    export const useUnsavedChangesGuard = (hasUnsavedChanges) => {
      const navigate = useNavigate();
      const [nextLocation, setNextLocation] = useState(null);
      const [blockerEnabled, setBlockerEnabled] = useState(hasUnsavedChanges);
    
      useEffect(() => {
        setBlockerEnabled(hasUnsavedChanges); // Sync blocker state with hasUnsavedChanges
      }, [hasUnsavedChanges]);
    
      const handleNavigationConfirm = useCallback(() => {
        if (nextLocation) {
          setBlockerEnabled(false); // Disable the blocker
          setTimeout(() => {
            navigate(nextLocation.pathname); // Navigate after ensuring blocker is unset
          }, 0);
    
          setNextLocation(null); // Clear the stored location
        }
      }, [nextLocation, navigate]);
    
      const handleNavigationCancel = useCallback(() => {
        setNextLocation(null); // Reset the stored location
      }, []);
    
      const blocker = useCallback(
        ({ nextLocation }) => {
          if (blockerEnabled && hasUnsavedChanges) {
            setNextLocation(nextLocation); // Store the location to navigate later
            return true; // Block navigation
          }
          return false; // Allow navigation
        },
        [blockerEnabled, hasUnsavedChanges, nextLocation]
      );
    
      useBlocker(blocker);
    
      return {
        handleNavigationConfirm,
        handleNavigationCancel,
        isBlocked: Boolean(nextLocation),
      };
    };

    To show or hide the popup you can subscribe to the isBlocked property

    const { handleNavigationConfirm, handleNavigationCancel, isBlocked } = useUnsavedChangesGuard(isFormDirty);
    
    useEffect(() => {
        if (isBlocked) {
          confirmRef.current?.showConfirm();
        } else {
          confirmRef.current?.hideConfirm();
        }
     }, [isBlocked]);
    
    // or if you have something like this
    <Confirm isOpen={isBlocked} onConfirm={handleNavigationConfirm} onCancel={handleNavigationCancel}>