44

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?

Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
Mark Kadlec
  • 8,036
  • 17
  • 60
  • 96

5 Answers5

24

It is too late but according to the React Router Documentation you can use preventing transition with helping of <prompt> component.

  <Prompt
      when={isBlocking}
      message={location =>
        `Are you sure you want to go to ${location.pathname}`
      }
    />

if isBlocking equal to true it shows a message. for more information you can read the documentation.

MBehtemam
  • 7,865
  • 15
  • 66
  • 108
  • 1
    @vsync [docs](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Prompt.md#when-bool) Instead of conditionally rendering a behind a guard, you can always render it but pass when={true} or when={false} to prevent or allow navigation accordingly. – Nerman Sep 17 '18 at 07:34
  • The `message` prop can also be a function that returns true or false, not just a string. This can help avoid the browser alert in favor of a custom modal or something else – vancy-pants Oct 20 '21 at 18:15
  • This is no longer the case with Router v6, unfortunately as Prompt was removed. – Nick George Jul 12 '22 at 17:19
8

react-router v6 no longer supports the Prompt component (they say that they hope to add it back once they have an acceptable implementation). However, react-router makes use of the history package which offers the following example for how to block transitions.

Note that to actually make this work in react router you have to replace the createBrowserHistory call with some hackery to make sure you are using the same history object as react router (see bottom of answer).


const history = createBrowserHistory();
let unblock = history.block((tx) => {
  // 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.
  let url = tx.location.pathname;
  if (window.confirm(`Are you sure you want to go to ${url}?`)) {
    // Unblock the navigation.
    unblock();

    // Retry the transition.
    tx.retry();
  }

You'll need to put this inside the appropriate useEffect hook and build the rest of the functionality that would have otherwise been provided by prompt. Note that this will also produce an (uncustomizable) warning if the user tries to navigate away but closing the tab or refreshing the page indicating that unsaved work may not be saved.

Please read the linked page as there are some drawbacks to using this functionality. Specifically, it adds an event listener to the beforeunload event which makes the page ineligable for the bfcache in firefox (though the code attempts to deregister the handler if the navigation is cancelled I'm not sure this restores salvageable status) I presume it's these issues which caused react-router to disable the Prompt component.

WARNING to access history in React Router 6 you need to follow something like the instructions here which is a bit of a hack. Initially, I assumed that you could just use createBrowserHistory to access the history object as that code is illustrated in the react router documentation but (a bit confusingly imo) it was intended only to illustrate the idea of what the history does.

Cory House
  • 14,235
  • 13
  • 70
  • 87
Peter Gerdes
  • 2,288
  • 1
  • 20
  • 28
6

I think the recommended approach has changed since Lazarev's answer, since his linked example is no longer currently in the examples folder. Instead, I think you should follow this example by defining:

componentWillMount() {
  this.props.router.setRouteLeaveHook(
    this.props.route,
    this.routerWillLeave
  )
},

And then define routerWillLeave to be a function that returns a string which will appear in a confirmation alert.

UPDATE

The previous link is now outdated and unavailable. In newer versions of React Router it appears there is a new component Prompt that can be used to cancel/control navigation. See this example

Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
T Patrick
  • 393
  • 4
  • 16
  • 1
    here is an example usingreact-router-dom Prompt: https://reacttraining.com/react-router/web/example/preventing-transitions – Peter Dec 12 '17 at 06:32
  • Looks like the latest version of `react-router-dom` uses a component called `Prompt`. You can see an [example here](https://reacttraining.com/react-router/web/example/preventing-transitions) – T Patrick Sep 07 '18 at 15:02
3

We're using React Router V5, and our site needed a custom prompt message to show up, and this medium article helped me understand how that was possible

TLDR: the <Prompt/> component from react-router-dom can accept a function as the message prop, and if that function returns true you'll continue in the navigation, and if false the navigation will be blocked

vancy-pants
  • 1,070
  • 12
  • 13
  • 1
    Above all the answers this one was soo good. It is so easy to understand and required lesser code to carry out the logic. – Ubaid Hussain Aug 13 '22 at 10:20
  • That medium article is a gem. Kudos! The link to live example from React Router is [here.](https://v5.reactrouter.com/web/example/preventing-transitions) – Kunal Phaltankar Sep 19 '22 at 15:59
0

React-router api provides a Transition object for such cases, you can create a hook in a willTransitionTo lifecycle method of the component, you are using. Something like (code taken from react-router examples on the github):

var Form = React.createClass({

  mixins: [ Router.Navigation ],

  statics: {
    willTransitionFrom: function (transition, element) {
      if (element.refs.userInput.getDOMNode().value !== '') {
        if (!confirm('You have unsaved information, are you sure you want to leave this page?')) {
          transition.abort();
        }
      }
    }
  },

  handleSubmit: function (event) {
    event.preventDefault();
    this.refs.userInput.getDOMNode().value = '';
    this.transitionTo('/');
  },

  render: function () {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <p>Click the dashboard link with text in the input.</p>
          <input type="text" ref="userInput" defaultValue="ohai" />
          <button type="submit">Go</button>
        </form>
      </div>
    );
  }
});
Alexandr Lazarev
  • 12,554
  • 4
  • 38
  • 47
  • Thank Lazarev, but in my code example, is there no way to just "stay on the page" and not go to the selected route? – Mark Kadlec May 10 '16 at 18:43
  • 1
    you wouldnt do it from the routes page, you would do it from your component, adding a `willTransitionFrom` hook, and checking if your form is dirty from there calling `transition.abort()` if it is – PhilVarg May 10 '16 at 18:49
  • I think you can't do it. Even if you can, it seems to be a bad approach, because even none of arguments are passed to `onLeave` event. If I'm not mistaken, `onLeave` most commonly is used for animations. By the way how do are you going to access `form`? – Alexandr Lazarev May 10 '16 at 19:00