6

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.

idkwhatsgoingon
  • 658
  • 4
  • 22
  • `react-router`/`react-router-dom` doesn't have this functionality. Could you provide a [minimal and complete code example](https://stackoverflow.com/help/minimal-reproducible-example) of the Vue code, and your attempt at something similar in React we can help? – Drew Reese Mar 22 '22 at 15:16
  • @DrewReese I cant provide an example. Basically I want to display a modal/alert when the user tries to leave a specific route. It seems it was possible in react-router v5 with Prompt/Blocker – idkwhatsgoingon Mar 22 '22 at 15:18
  • 2
    I see. Yes, currently that functionality has been excluded from RRDv6 (*supposedly it will return, TBD*), but I imagine you could replicate something close to it with a [custom router & history object](https://stackoverflow.com/a/70646548/8690857) to listen for route changes, specifically the POP action, possibly combined with listening for the [beforeunload](https://stackoverflow.com/a/64967211/8690857) event. – Drew Reese Mar 22 '22 at 16:01

4 Answers4

16

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());
idkwhatsgoingon
  • 658
  • 4
  • 22
  • 1
    Hey bro, is there anyway to not using `any`? Cause my eslint rule (`no-explicit-any`). – Tuan Aug 11 '22 at 08:40
  • 1
    @Tuan You can remove the as any casting, if you import the correct Navigator type. I didnt initially see that you need to import that type from react-router, but i just checked and that should work. I have updated my answer accordingly – idkwhatsgoingon Aug 11 '22 at 12:27
  • I tried implementing this but Transition no longer exists on 'history' – swedish_junior_dev Mar 21 '23 at 14:45
  • @swedish_junior_dev Cant see anything about that mentioned in the [5.3 changelog for "history"](https://github.com/remix-run/history/releases) (I used 5.2 version of the npm package "history" and that worked fine. What version are you using? – idkwhatsgoingon Mar 22 '23 at 13:10
  • 1
    @idkwhatsgoingon I had version 4.9.0. Updated to latest and the error is gone. Thx! – swedish_junior_dev Mar 22 '23 at 14:04
3

Using the other solutions listed here against react-router v6.8.0, I got errors stating that navigator.block is undefined when I tried pass a true into the second argument of usePrompt

After a little digging on the react-router GitHub pages, I found the maintainer's docs explaining why the changes happened but I also found this little gem pushed in recently.

While there currently isn't a full-fledged feature in the library, the unstable_useBlocker func can get the job done.

Here's a rough example that worked for me:

import { useEffect, useState } from 'react';
import { unstable_useBlocker } from 'react-router-dom';

const MyComponent = () => {
  const [shouldBlockLeaving, setShouldBlockLeaving] = useState<boolean>(false);

  const blocker = unstable_useBlocker(shouldBlockLeaving);

  if (blocker.state === 'blocked') {
    if (window.confirm('You have unsaved changes, are you sure you want to leave?')) {
      blocker.proceed?.();
    } else {
      blocker.reset?.();
    }
  }

  // reset the blocker if the user cleans the form
  useEffect(() => {
    if (blocker.state === 'blocked' && !shouldBlockLeaving) {
      blocker.reset();
    }
  }, [blocker, shouldBlockLeaving]);

  return (<>
    ...
  </>)
} 

Note that this is an incomplete example because I have nothing setting the shouldBlockLeaving state to true, but hopefully this is easy to extrapolate from.

Here's a more thorough example on Stackblitz

pseudosma
  • 146
  • 3
  • unstable_useBlocker doesn't handle browser triggered events anymore. Have you found any way to prevent refreshing or closing tab that doesn't use event `onbeforeunload` that is not implemented in all browser? – Anthony Collin-Nadeau Apr 06 '23 at 19:05
  • 1
    No, unfortunately, but from my testing the original code using I was migrating from had this exact same limitation, so it wasn't really a loss of functionality for my case, just continuation of the same "good enough for now" solution. There's a lot of chatter on the react-router GitHub page about this feature being re-added so I'm really hoping a future version addresses the problem you cited. https://github.com/remix-run/react-router/issues/8139 – pseudosma Apr 07 '23 at 21:39
  • This is pretty cool. I modified to `blocker.reset()` everytime when `shouldBlockLeaving` is `true` and stored the target location in `redux` from `blocker.location.pathname`. Created a custom confirm dialog and if the user confirmed, then navigated to the stored location. – Eqzt111 May 28 '23 at 19:05
-1

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]);

sadaf khan
  • 38
  • 4
-3

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

James
  • 429
  • 1
  • 8
  • 17