194

I want my ReactJS app to notify a user when navigating away from a specific page. Specifically a popup message that reminds him/her to do an action:

"Changes are saved, but not published yet. Do that now?"

Should i trigger this on react-router globally, or is this something that can be done from within the react page / component?

I havent found anything on the latter, and i'd rather avoid the first. Unless its the norm of course, but that makes me wonder how to do such a thing without having to add code to every other possible page the user can go to..

Any insights welcome, thanks!

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
Barry Staes
  • 3,890
  • 4
  • 24
  • 30
  • 3
    I don't now if this is what you are searching for, but you can do sth. like this `componentWillUnmount() { if (confirm('Changes are saved, but not published yet. Do that now?')) { // publish and go away from a specific page } else { // do nothing and go away from a specific page } }` so you can call your publish function bevor leaving the page – Rico Berger Sep 29 '15 at 13:54

11 Answers11

235

react-router v4 introduces a new way to block navigation using Prompt. Just add this to the component that you would like to block:

import { Prompt } from 'react-router'

const MyComponent = () => (
  <>
    <Prompt
      when={shouldBlockNavigation}
      message='You have unsaved changes, are you sure you want to leave?'
    />
    {/* Component JSX */}
  </>
)

This will block any routing, but not page refresh or closing. To block that, you'll need to add this (updating as needed with the appropriate React lifecycle):

componentDidUpdate = () => {
  if (shouldBlockNavigation) {
    window.onbeforeunload = () => true
  } else {
    window.onbeforeunload = undefined
  }
}

onbeforeunload has various support by browsers.

jcady
  • 3,850
  • 2
  • 21
  • 21
  • 18
    This results in two very different looking alerts though. – XanderStrike May 03 '18 at 16:09
  • 3
    @XanderStrike You could try to style the prompt alert to mimic the browser default alert. Unfortunately, there's no way to style the `onberforeunload` alert. – jcady May 03 '18 at 18:17
  • Is there any way to show the same Prompt component once the user click on a CANCEL button for example? – Rene Enriquez Sep 11 '18 at 17:46
  • 1
    @ReneEnriquez `react-router` doesn't support that out of the box (assuming the cancel button doesn't trigger any route changes). You can create your own modal to mimic the behavior though. – jcady Oct 09 '18 at 17:11
  • 12
    If you end up using `onbeforeunload `, you'll want to clean it up when your component unmounts. `componentWillUnmount() { window.onbeforeunload = null; }` – cdeutsch Jan 29 '19 at 00:13
  • If I click 'cancel', I get only this component rendered, not it's container like menu bar at the top – polina-c May 01 '19 at 01:37
  • Is there a way to stop prompt from showing while changing query parameters? If you have /create/asc/1 and you use history push or replace /create/asc/2, the prompt will show, anyway to only show when the component is going to unmount? – blackops Jul 10 '19 at 19:33
  • @blackops you have access to the future location.pathname in the Prompt component and the current location.pathname in your current component using props.location.pathname, you can use this information to return true from message if you don't want to show the prompt – gaurav5430 Dec 24 '19 at 19:09
  • I keep getting `Uncaught TypeError: window.onbeforeunload is not a function` when I try adding `window.onbeforeunload( (e) => { return 'Hello?' })` instead of my React hook. – Badrush Jan 27 '20 at 16:42
  • 1
    Fragment can be shortened to just `<>>` – Brooks DuBois Apr 26 '21 at 21:39
  • I'm using this approach: so Prompt inside a component and that component is used inside the Router. It seems to work fine, and then out of the blue it won't work, I get "You should not use outside of ". Then it will work again and so on. Any ideas? :( – amihaiemil Jun 17 '21 at 13:33
  • 1
    Opening a file upload prompt in Chrome triggers the exit warning prompt for me. I couldn't find any documentaton around how to ignore it – Angel Venchev Oct 18 '22 at 21:52
  • @AngelVenchev You can always unset the `onbeforeload` right before you open the upload prompt and then re-set it once the user has selected the file or canceled. – jcady Oct 19 '22 at 22:21
  • Actually it turned out that the button that triggered the file upload was an `` and the router was capturing that as a redirect therefore triggering the prompt – Angel Venchev Oct 20 '22 at 14:31
60

In react-router v2.4.0 or above and before v4 there are several options

  1. Add function onLeave for Route
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
    
  1. Use function setRouteLeaveHook for componentDidMount

You can prevent a transition from happening or prompt the user before leaving a route with a leave hook.

const Home = withRouter(
  React.createClass({

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

    routerWillLeave(nextLocation) {
      // return false to prevent a transition w/o prompting the user,
      // or return a string to allow the user to decide:
      // return `null` or nothing to let other hooks to be executed
      //
      // NOTE: if you return true, other hooks will not be executed!
      if (!this.state.isSaved)
        return 'Your work is not saved! Are you sure you want to leave?'
    },

    // ...

  })
)

Note that this example makes use of the withRouter higher-order component introduced in v2.4.0.

However these solution doesn't quite work perfectly when changing the route in URL manually

In the sense that

  • we see the Confirmation - ok
  • contain of page doesn't reload - ok
  • URL doesn't changes - not okay

For react-router v4 using Prompt or custom history:

However in react-router v4 , its rather easier to implement with the help of Prompt from'react-router

According to the documentation

Prompt

Used to prompt the user before navigating away from a page. When your application enters a state that should prevent the user from navigating away (like a form is half-filled out), render a <Prompt>.

import { Prompt } from 'react-router'

<Prompt
  when={formIsHalfFilledOut}
  message="Are you sure you want to leave?"
/>

message: string

The message to prompt the user with when they try to navigate away.

<Prompt message="Are you sure you want to leave?"/>

message: func

Will be called with the next location and action the user is attempting to navigate to. Return a string to show a prompt to the user or true to allow the transition.

<Prompt message={location => (
  `Are you sure you want to go to ${location.pathname}?`
)}/>

when: bool

Instead of conditionally rendering a <Prompt> behind a guard, you can always render it but pass when={true} or when={false} to prevent or allow navigation accordingly.

In your render method you simply need to add this as mentioned in the documentation according to your need.

UPDATE:

In case you would want to have a custom action to take when user is leaving page, you can make use of custom history and configure your Router like

history.js

import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()

... 
import { history } from 'path/to/history';
<Router history={history}>
  <App/>
</Router>

and then in your component you can make use of history.block like

import { history } from 'path/to/history';
class MyComponent extends React.Component {
   componentDidMount() {
      this.unblock = history.block(targetLocation => {
           // take your action here     
           return false;
      });
   }
   componentWillUnmount() {
      this.unblock();
   }
   render() {
      //component render here
   }
}
Vebjorn Ljosa
  • 17,438
  • 13
  • 70
  • 88
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 2
    Even if you use `` the URL gets changed when you press Cancel on the prompt. Related issue: https://github.com/ReactTraining/react-router/issues/5405 – Sahan Serasinghe Jun 13 '18 at 02:12
  • 1
    I see this question is still quite popular. Since the top answers are about for old deprecated versions, i set this newer answer as the accepted one. Links to old documentation (and upgrade guides) are now available at https://github.com/ReactTraining/react-router – Barry Staes Sep 10 '18 at 10:40
  • 2
    onLeave is fired AFTER a route change is confirmed. Can you elaborate about how to cancel a navigation in onLeave? – schlingel Sep 13 '18 at 12:28
  • 1
    `history.block()` is a great way to run any "before route" logic, e.g. storing the scroll position. – Ricky Boyce Oct 19 '21 at 22:07
22

For react-router 2.4.0+

NOTE: It is advisable to migrate all your code to the latest react-router to get all the new goodies.

As recommended in the react-router documentation:

One should use the withRouter higher order component:

We think this new HoC is nicer and easier, and will be using it in documentation and examples, but it is not a hard requirement to switch.

As an ES6 example from the documentation:

import React from 'react'
import { withRouter } from 'react-router'

const Page = React.createClass({

  componentDidMount() {
    this.props.router.setRouteLeaveHook(this.props.route, () => {
      if (this.state.unsaved)
        return 'You have unsaved information, are you sure you want to leave this page?'
    })
  }

  render() {
    return <div>Stuff</div>
  }

})

export default withRouter(Page)
psiyumm
  • 6,437
  • 3
  • 29
  • 50
  • 4
    What does the RouteLeaveHook callback do? Does it prompt the user using a in-built modal? What if you want a custom modal – Learner Mar 02 '17 at 06:02
  • Like @Learner: How to do this with a customized confirm box like vex or SweetAlert or other non-blocking dialog? – olefrank Mar 18 '17 at 12:16
  • I am having the following error :- TypeError: Cannot read property '__id__' of undefined. Any suggestion – Mustafa Mamun Oct 13 '17 at 10:54
  • @MustafaMamun That seems to an unrelated problem and you'll mostly want to create a new question explaining the details. – psiyumm Oct 13 '17 at 15:52
  • I was having the issue while trying to use the solution. The component has to be directly connected to the router then this solution works. I found a better answer there https://stackoverflow.com/questions/39103684/getting-an-error-on-using-setrouteleavehook-withrouter – Mustafa Mamun Oct 16 '17 at 08:37
  • I'm using typescript and this.props.router is undefined! I'm a bit of a nooby with react but how do I get the router prop defined so I can use it? – Choco Aug 09 '18 at 06:28
9

For react-router v3.x

I had the same issue where I needed a confirmation message for any unsaved change on the page. In my case, I was using React Router v3, so I could not use <Prompt />, which was introduced from React Router v4.

I handled 'back button click' and 'accidental link click' with the combination of setRouteLeaveHook and history.pushState(), and handled 'reload button' with onbeforeunload event handler.

setRouteLeaveHook (doc) & history.pushState (doc)

  • Using only setRouteLeaveHook was not enough. For some reason, the URL was changed although the page remained the same when 'back button' was clicked.

      // setRouteLeaveHook returns the unregister method
      this.unregisterRouteHook = this.props.router.setRouteLeaveHook(
        this.props.route,
        this.routerWillLeave
      );
    
      ...
    
      routerWillLeave = nextLocation => {
        // Using native 'confirm' method to show confirmation message
        const result = confirm('Unsaved work will be lost');
        if (result) {
          // navigation confirmed
          return true;
        } else {
          // navigation canceled, pushing the previous path
          window.history.pushState(null, null, this.props.route.path);
          return false;
        }
      };
    

onbeforeunload (doc)

  • It is used to handle 'accidental reload' button

    window.onbeforeunload = this.handleOnBeforeUnload;
    
    ...
    
    handleOnBeforeUnload = e => {
      const message = 'Are you sure?';
      e.returnValue = message;
      return message;
    }
    

Below is the full component that I have written

  • note that withRouter is used to have this.props.router.
  • note that this.props.route is passed down from the calling component
  • note that currentState is passed as prop to have initial state and to check any change

    import React from 'react';
    import PropTypes from 'prop-types';
    import _ from 'lodash';
    import { withRouter } from 'react-router';
    import Component from '../Component';
    import styles from './PreventRouteChange.css';
    
    class PreventRouteChange extends Component {
      constructor(props) {
        super(props);
        this.state = {
          // initialize the initial state to check any change
          initialState: _.cloneDeep(props.currentState),
          hookMounted: false
        };
      }
    
      componentDidUpdate() {
    
       // I used the library called 'lodash'
       // but you can use your own way to check any unsaved changed
        const unsaved = !_.isEqual(
          this.state.initialState,
          this.props.currentState
        );
    
        if (!unsaved && this.state.hookMounted) {
          // unregister hooks
          this.setState({ hookMounted: false });
          this.unregisterRouteHook();
          window.onbeforeunload = null;
        } else if (unsaved && !this.state.hookMounted) {
          // register hooks
          this.setState({ hookMounted: true });
          this.unregisterRouteHook = this.props.router.setRouteLeaveHook(
            this.props.route,
            this.routerWillLeave
          );
          window.onbeforeunload = this.handleOnBeforeUnload;
        }
      }
    
      componentWillUnmount() {
        // unregister onbeforeunload event handler
        window.onbeforeunload = null;
      }
    
      handleOnBeforeUnload = e => {
        const message = 'Are you sure?';
        e.returnValue = message;
        return message;
      };
    
      routerWillLeave = nextLocation => {
        const result = confirm('Unsaved work will be lost');
        if (result) {
          return true;
        } else {
          window.history.pushState(null, null, this.props.route.path);
          if (this.formStartEle) {
            this.moveTo.move(this.formStartEle);
          }
          return false;
        }
      };
    
      render() {
        return (
          <div>
            {this.props.children}
          </div>
        );
      }
    }
    
    PreventRouteChange.propTypes = propTypes;
    
    export default withRouter(PreventRouteChange);
    

Please let me know if there is any question :)

Barry Staes
  • 3,890
  • 4
  • 24
  • 30
Sang Yun Park
  • 459
  • 7
  • 14
8

That's how you can show a message when user switch to another route or leave current page and go to another URL

import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
import { Prompt } from 'react-router-dom'
import { useTranslation } from 'react-i18next'


const LeavePageBlocker = ({ when }) => {
  const { t } = useTranslation()
  const message = t('page_has_unsaved_changes')

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

    const beforeUnloadCallback = (event) => {
      event.preventDefault()
      event.returnValue = message
      return message
    }

    window.addEventListener('beforeunload', beforeUnloadCallback)
    return () => {
      window.removeEventListener('beforeunload', beforeUnloadCallback)
    }
  }, [when, message])

  return <Prompt when={when} message={message} />
}

LeavePageBlocker.propTypes = {
  when: PropTypes.bool.isRequired,
}

export default LeavePageBlocker

Your page:

const [dirty, setDirty] = setState(false)
...
return (
  <>
    <LeavePageBlocker when={dirty} />
    ...
  </>
)
3

Using history.listen

For example like below:

In your component,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
ravibagul91
  • 20,072
  • 5
  • 36
  • 59
  • 10
    Hi, welcome to Stack Overflow. When answering a question that already has many answers, please be sure to add some additional insight into why the response you're providing is substantive and not simply echoing what's already been vetted by the original poster. This is especially important in "code-only" answers such as the one you've provided. – chb Mar 11 '19 at 09:11
2

For react-router v0.13.x with react v0.13.x:

this is possible with the willTransitionTo() and willTransitionFrom() static methods. For newer versions, see my other answer below.

From the react-router documentation:

You can define some static methods on your route handlers that will be called during route transitions.

willTransitionTo(transition, params, query, callback)

Called when a handler is about to render, giving you the opportunity to abort or redirect the transition. You can pause the transition while you do some asynchonous work and call callback(error) when you're done, or omit the callback in your argument list and it will be called for you.

willTransitionFrom(transition, component, callback)

Called when an active route is being transitioned out giving you an opportunity to abort the transition. The component is the current component, you'll probably need it to check its state to decide if you want to allow the transition (like form fields).

Example

  var Settings = React.createClass({
    statics: {
      willTransitionTo: function (transition, params, query, callback) {
        auth.isLoggedIn((isLoggedIn) => {
          transition.abort();
          callback();
        });
      },

      willTransitionFrom: function (transition, component) {
        if (component.formHasUnsavedData()) {
          if (!confirm('You have unsaved information,'+
                       'are you sure you want to leave this page?')) {
            transition.abort();
          }
        }
      }
    }

    //...
  });

For react-router 1.0.0-rc1 with react v0.14.x or later:

this should be possible with the routerWillLeave lifecycle hook. For older versions, see my answer above.

From the react-router documentation:

To install this hook, use the Lifecycle mixin in one of your route components.

  import { Lifecycle } from 'react-router'

  const Home = React.createClass({

    // Assuming Home is a route component, it may use the
    // Lifecycle mixin to get a routerWillLeave method.
    mixins: [ Lifecycle ],

    routerWillLeave(nextLocation) {
      if (!this.state.isSaved)
        return 'Your work is not saved! Are you sure you want to leave?'
    },

    // ...

  })

Things. may change before the final release though.

Barry Staes
  • 3,890
  • 4
  • 24
  • 30
2

You can use this prompt.

import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link, Prompt } from "react-router-dom";

function PreventingTransitionsExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Form</Link>
          </li>
          <li>
            <Link to="/one">One</Link>
          </li>
          <li>
            <Link to="/two">Two</Link>
          </li>
        </ul>
        <Route path="/" exact component={Form} />
        <Route path="/one" render={() => <h3>One</h3>} />
        <Route path="/two" render={() => <h3>Two</h3>} />
      </div>
    </Router>
  );
}

class Form extends Component {
  state = { isBlocking: false };

  render() {
    let { isBlocking } = this.state;

    return (
      <form
        onSubmit={event => {
          event.preventDefault();
          event.target.reset();
          this.setState({
            isBlocking: false
          });
        }}
      >
        <Prompt
          when={isBlocking}
          message={location =>
            `Are you sure you want to go to ${location.pathname}`
          }
        />

        <p>
          Blocking?{" "}
          {isBlocking ? "Yes, click a link or the back button" : "Nope"}
        </p>

        <p>
          <input
            size="50"
            placeholder="type something to block transitions"
            onChange={event => {
              this.setState({
                isBlocking: event.target.value.length > 0
              });
            }}
          />
        </p>

        <p>
          <button>Submit to stop blocking</button>
        </p>
      </form>
    );
  }
}

export default PreventingTransitionsExample;
Jaskaran Singh
  • 2,392
  • 24
  • 39
1

import React, {useEffect} from 'react';
import { useLocation} from 'react-router';


const prevLocation = useLocation().pathname; 

useEffect(() => {
  const unlisten = history.listen((location) => {
    if (unsavedCondition && location.pathname !== prevLocation) {
      history.push(prevLocation) 
      
      //Do something, like display confirmation dialog!
    }
  });

  return () => {
    unlisten()
  }
},[])
Ivan S.
  • 94
  • 1
  • 5
0

May be you can use componentWillUnmount() to do anything before the user leaving the page. If you are using functional components, then you can just do the same with useEffect() hook. The hook accepts a function that returns a Destructor, which is similar to what componentWillUnmount() can do.

Credit goes to this article

0

For react-router v3.x and functional components we can use such hook:

import { useEffect, useState } from "react";
import { usePrevious } from "./usePrevious";

const useConfirmation = ({ router, route, items }) => {
  const [needConfirmation, setNeedConfirmation] = useState(false);
// You can use for the prevState any value, in my case it was length of items
      const prevItemsLength = usePrevious({ length: items?.length });
    
  const commonMsg = 
    "you-have-unsaved-information-are-you-sure-you-want-to-leave-this-page";

  const onBeforeUnload = (e) => {
    if (needConfirmation) {
      e.returnValue = true;
      return commonMsg;
    }

    return null;
  };

  const routerWillLeave = () => {
    if (needConfirmation) {
      return commonMsg;
    }

    return true;
  };

  useEffect(() => {
    if (prevItemsLength?.length > items?.length) {
      setNeedConfirmation(() => true);
    }
  }, [items]);

  useEffect(() => {
    if (needConfirmation) {
      window.addEventListener("beforeunload", onBeforeUnload);
      router.setRouteLeaveHook(route, routerWillLeave);
    } else {
      router.setRouteLeaveHook(route, () => {});
      window.removeEventListener("beforeunload", onBeforeUnload);
    }

    return () => window.removeEventListener("beforeunload", onBeforeUnload);
  }, [needConfirmation]);

  return [needConfirmation, setNeedConfirmation];
};

export { useConfirmation };

Then, in another file we disable confirmation after data was saved:

  const [needConfirm, setNeedConfirm] = useConfirmation({
    router,
    route,
    items,
  });

  const saveChanges = useCallback(() => {
    //before turning off confirmation, there may be a request to the API to save our data
    //if request was success then we set the 'needConfirm' value to 'false'
    setNeedConfirm(() => false);
  }); 

And ofc here information about 'usePrevious' hook: How to compare oldValues and newValues on React Hooks useEffect?

carrych
  • 1
  • 1