Detecting user leaving page with react-router
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.
In react-router v2.4.0
or above and before v4
there are several options
- Add function
onLeave
forRoute
<Route
path="/home"
onEnter={ auth }
onLeave={ showConfirm }
component={ Home }
>
- Use function
setRouteLeaveHook
forcomponentDidMount
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 passwhen={true}
orwhen={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
}
}
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)
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 changeimport 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 :)