Published 15 Feb, 2022

How To Check Form Is Dirty Before Leaving Page/Route In React Router v6?

Category React
Modified : Feb 01, 2023

There are 3 suggested solutions in this post and each one is listed below with a detailed description on the basis of most helpful answers as shared by the users. I have tried to cover all the aspects as briefly as possible covering topics such as Reactjs, React Redux, React Router, Material Ui and a few others. I have categorized the possible solutions in sections for a clear and precise explanation. Please consider going through all the sections to better understand the solutions.

Solution 1

This answer uses router v6.

  1. You can use usePrompt.
  • usePrompt will show the confirm modal/popup when you go to another route i.e. on mount.
  • A generic alert with message when you try to close the browser. It handles beforeunload internally
usePrompt("Hello from usePrompt -- Are you sure you want to leave?", isBlocking);
  1. You can use useBlocker
  • useBlocker will simply block user when attempting to navigating away i.e. on unmount
  • A generic alert with message when you try to close the browser. It handles beforeunload internally
useBlocker(
    () => "Hello from useBlocker -- are you sure you want to leave?",
    isBlocking
  );

Demo for both 1 & 2

  1. You can also use beforeunload. But you have to do your own logic. See an example here

Solution 2

Just adding an additional answer for React Router v6 users.

As of v6.0.0-beta - useBlocker and usePrompt were removed (to be added back in at a later date).

It was suggsested if we need them in v6.0.2 (current version at the time of writing) that we should use existing code as an example.

Here is the code directly from the the alpha for these hooks.

So to add the hooks back in would be this code (anywhere in your app for usage): ** I only copied the code for react-router-dom - if you're using native, then you'll need to check the above link for the other usePrompt hook

/**
 * These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
 * Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
 * Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
 */
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
 * Blocks all navigation attempts. This is useful for preventing the page from
 * changing until some condition is met, like saving form data.
 *
 * @param  blocker
 * @param  when
 * @see https://reactrouter.com/api/useBlocker
 */
export function useBlocker( blocker, when = true ) {
    const { navigator } = useContext( NavigationContext );

    useEffect( () => {
        if ( ! when ) return;

        const unblock = navigator.block( ( tx ) => {
            const autoUnblockingTx = {
                ...tx,
                retry() {
                    // Automatically unblock the transition so it can play all the way
                    // through before retrying it. TODO: Figure out how to re-enable
                    // this block if the transition is cancelled for some reason.
                    unblock();
                    tx.retry();
                },
            };

            blocker( autoUnblockingTx );
        } );

        return unblock;
    }, [ navigator, blocker, when ] );
}
/**
 * Prompts the user with an Alert before they leave the current screen.
 *
 * @param  message
 * @param  when
 */
export function usePrompt( message, when = true ) {
    const blocker = useCallback(
        ( tx ) => {
            // eslint-disable-next-line no-alert
            if ( window.confirm( message ) ) tx.retry();
        },
        [ message ]
    );

    useBlocker( blocker, when );
}

Then the usage would be:

const MyComponent = () => {
    const formIsDirty = true; // Condition to trigger the prompt.
    usePrompt( 'Leave screen?', formIsDirty );
    return (
        <div>Hello world</div> 
    );
};


Solution 3

@Devb your question and update were super helpful and saved me a lot of time. Thank you! created a HOC based on your code. might be useful to someone. props on Wrapped Component:

  • setPreventNavigation - sets when to block navigation

  • provideLeaveHandler - sets the function that will run when you try to change a route and you are blocked for navigation

  • confirmNavigation - continue navigation

  • cancelNavigation - stop Navigation

     import React, { useEffect, useState, useCallback } from 'react'
     import { useNavigate, useBlocker, useLocation } from 'react-router-dom'
    
     export default function withPreventNavigation(WrappedComponent) {
       return function preventNavigation(props) {
         const navigate = useNavigate()
         const location = useLocation()
         const [lastLocation, setLastLocation] = useState(null)
         const [confirmedNavigation, setConfirmedNavigation] = useState(false)
         const [shouldBlock, setShouldBlock] = useState(false)
    
        let handleLeave = null
    
         const cancelNavigation = useCallback(() => {
           setshouldBlock(false)
         },[])
    
         const handleBlockedNavigation = useCallback(
           nextLocation => {
             if (
               !confirmedNavigation &&
               nextLocation.location.pathname !== location.pathname
             ) {
               handleLeave(nextLocation)
               setLastLocation(nextLocation)
               return false
             }
             return true
           },
           [confirmedNavigation]
         )
    
         const confirmNavigation = useCallback(() => {
           setConfirmedNavigation(true)
         }, [])
    
         useEffect(() => {
           if (confirmedNavigation && lastLocation) {
             navigate(lastLocation.location.pathname)
           }
         }, [confirmedNavigation, lastLocation])
    
         const provideLeaveHandler = handler => {
           handleLeave = handler
         }
    
         useBlocker(handleBlockedNavigation, shouldBlock)
    
         return (
           <WrappedComponent
             {...props}
             provideLeaveHandler=
             setPreventNavigation=
             confirmNavigation=
             cancelNavigation= />
         )
       }
     }
    

Final Words

These were some of the solutions I found worth sharing. There are well a lot of alternatives around but I have tried and tested these for a while now and so I found them worth sharing here. I hope it fulfills the purpose you're looking to utilize them for.

Ubuntu is an ancient African word, meaning "can’t configure Debian"
Unknown