React Router v.6 usePrompt TypeScript

Viewed 2256

I am basically trying to intercept route changes. Maybe something equivalent of vue's beforeEach in React Router v6 could be useful as React Router v.6 does not include usePrompt.

BEFORE each route change I want to do some logic - the logic might need to interrupt or even change the end route based on the result.

I have searched around but I really can't find something that solves this specific problem.

Thanks in advance.

3 Answers

Currently they have removed the usePrompt from the react-router v6.

I found a solution from ui.dev and added TypeScript support, and am now using that until the react-router will bring back the usePrompt/useBlocker hooks

import { History, Transition } from 'history';
import { useCallback, useContext, useEffect } from "react";
import { Navigator } from 'react-router';
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";

type ExtendNavigator = Navigator & Pick<History, "block">;
export function useBlocker(blocker: (tx: Transition) => void, when = true) {
    const { navigator } = useContext(NavigationContext);

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

        const unblock = (navigator as ExtendNavigator).block((tx) => {
            const autoUnblockingTx = {
                ...tx,
                retry() {
                    unblock();
                    tx.retry();
                },
            };

            blocker(autoUnblockingTx);
        });

        return unblock;
    }, [navigator, blocker, when]);
}

export default function usePrompt(message: string, when = true) {
    const blocker = useCallback((tx: Transition) => {
        if (window.confirm(message)) tx.retry();
    }, [message]);

    useBlocker(blocker, when);
}

This can then be used in any view/component where you would like a "A you sure you want to leave?"-message displayed when the condition is true.

usePrompt("Do you want to leave?", isFormDirty());

Yes usePrompt and useBlock has been removed, but you can achieve same thing using history.block, here is the working example for blocking navigation using history.block with custom modal in React Router Dom V5

import { useHistory } from "react-router-dom";
import { UnregisterCallback } from "history";
...

type Prop = {
  verify?: {
    blockRoute?: (nextRoute: string) => boolean;
  }
};

...
// in the component where you want to show confirmation modal on any nav change

const history = useHistory();
const unblock = useRef<UnregisterCallback>();
const onConfirmExit = () => {
  /**
   * if user confirms to exit, we can allow the navigation
   */

  // Unblock the navigation.
  unblock?.current?.();
  // Proceed with the blocked navigation
  goBack();
};

useEffect(() => {
  /**
   * Block navigation and register a callback that
   * fires when a navigation attempt is blocked.
   */
  unblock.current = history.block(({ pathname: to }) => {
   /**
    * Simply allow the transition to pass immediately,
    * if user does not want to verify the navigate away action,
    * or if user is allowed to navigate to next route without blocking.
    */
   if (!verify || !verify.blockRoute?.(to)) return undefined;

   /**
    * Navigation was blocked! Let's show a confirmation dialog
    * so the user can decide if they actually want to navigate
    * away and discard changes they've made in the current page.
    */
   showConfirmationModal();
   // prevent navigation
   return false;
});

// just in case theres an unmount we can unblock if it exists
  return unblock.current;
}, [history]);

Here is a JS example of the react-route-dom v6 usePrompt if you're not using TS.

import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';

export function useBlocker( blocker, when = true ) {
    const { navigator } = useContext( NavigationContext );

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

        const unblock = navigator.block( ( tx ) => {
            const autoUnblockingTx = {
                ...tx,
                retry() {
                    unblock();
                    tx.retry();
                },
            };

            blocker( autoUnblockingTx );
        } );

        return unblock;
    }, [ navigator, blocker, 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 implementation would be...

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

Here's the article with the example

Related