Detecting user leaving page with react-router
ReactjsReact RouterReactjs Problem Overview
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!
Reactjs Solutions
Solution 1 - Reactjs
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.
Solution 2 - Reactjs
In react-router v2.4.0
or above and before v4
there are several options
<Route
path="/home"
onEnter={ auth }
onLeave={ showConfirm }
component={ Home }
>
2. 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'
>
> 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
}
}
Solution 3 - Reactjs
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)
Solution 4 - Reactjs
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 :)
Solution 5 - Reactjs
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);
});
}
Solution 6 - Reactjs
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.
Solution 7 - Reactjs
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;
Solution 8 - Reactjs
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} />
...
</>
)
Solution 9 - Reactjs
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