使用反应路由器检测用户离开页面
Detecting user leaving page with react-router
我希望我的 ReactJS 应用在离开特定页面时通知用户。特别是提醒 him/her 执行操作的弹出消息:
"Changes are saved, but not published yet. Do that now?"
我应该在 react-router
全局触发这个,还是可以在 React 页面/组件中完成?
我还没有找到关于后者的任何信息,我宁愿避免使用第一个。除非它当然是常态,但这让我想知道如何在不必向用户可以访问的所有其他可能页面添加代码的情况下做这样的事情..
欢迎任何见解,谢谢!
对于 react-router
v0.13.x 和 react
v0.13.x:
这可以通过 willTransitionTo()
和 willTransitionFrom()
静态方法实现。对于较新的版本,请参阅下面我的其他答案。
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();
}
}
}
}
//...
});
对于 react-router
1.0.0-rc1 with react
v0.14.x 或更高版本:
这应该可以通过 routerWillLeave
生命周期挂钩实现。对于旧版本,请参阅上面的回答。
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?'
},
// ...
})
东西。不过在最终版本发布之前可能会发生变化。
对于react-router
2.4.0+
注意:建议将所有代码迁移到最新的 react-router
以获得所有新功能。
react-router documentation中的推荐:
应该使用 withRouter
高阶组件:
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.
作为文档中的 ES6 示例:
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)
在react-router v2.4.0
或以上v4
之前有几个选项
<Route
path="/home"
onEnter={ auth }
onLeave={ showConfirm }
component={ Home }
>
您可以使用 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?'
},
// ...
})
)
请注意,此示例使用了 v2.4.0.
中引入的 withRouter
高阶组件
然而,当手动更改 URL 中的路线时,这些解决方案并不能很好地工作
从某种意义上说
- 我们看到确认 - 好的
- 页面包含未重新加载 - 正常
- URL 没有变化 - 不行
对于 react-router v4
使用提示或自定义历史记录:
然而在 react-router v4
中,在 Prompt
from'react-router
的帮助下更容易实现
根据文档
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.
在您的渲染方法中,您只需根据需要按照文档中的说明添加它。
更新:
如果您希望在用户离开页面时执行自定义操作,您可以使用自定义历史记录并像这样配置路由器
history.js
import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()
...
import { history } from 'path/to/history';
<Router history={history}>
<App/>
</Router>
然后在您的组件中,您可以使用 history.block
之类的
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
}
}
react-router
v4 引入了一种使用 Prompt
阻止导航的新方法。只需将其添加到您要阻止的组件中:
import { Prompt } from 'react-router'
const MyComponent = () => (
<>
<Prompt
when={shouldBlockNavigation}
message='You have unsaved changes, are you sure you want to leave?'
/>
{/* Component JSX */}
</>
)
这将阻止任何路由,但不会阻止页面刷新或关闭。要阻止它,您需要添加它(根据需要使用适当的 React 生命周期进行更新):
componentDidUpdate = () => {
if (shouldBlockNavigation) {
window.onbeforeunload = () => true
} else {
window.onbeforeunload = undefined
}
}
onbeforeunload 有各种浏览器支持。
对于react-router
v3.x
我遇到了同样的问题,我需要为页面上任何未保存的更改发送确认消息。在我的例子中,我使用的是 React Router v3,所以我不能使用 <Prompt />
,它是从 React Router v4 引入的。
我用 setRouteLeaveHook
和 history.pushState()
的组合处理了 'back button click' 和 'accidental link click',用 onbeforeunload
事件处理程序处理了 'reload button'。
setRouteLeaveHook (doc) & history.pushState (doc)
仅使用 setRouteLeaveHook 是不够的。出于某种原因,尽管单击 'back button' 时页面保持不变,但 URL 已更改。
// 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)
用于处理'accidental reload'按钮
window.onbeforeunload = this.handleOnBeforeUnload;
...
handleOnBeforeUnload = e => {
const message = 'Are you sure?';
e.returnValue = message;
return message;
}
以下是我编写的完整组件
- 请注意,withRouter 用于
this.props.router
。
- 请注意,
this.props.route
是从调用组件向下传递的
请注意,currentState
作为 prop 传递以具有初始状态并检查任何更改
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);
如果有任何问题,请告诉我:)
使用history.listen
例如如下:
在您的组件中,
componentWillMount() {
this.props.history.listen(() => {
// Detecting, user has changed URL
console.info(this.props.history.location.pathname);
});
}
你可以使用这个提示。
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;
也许您可以使用 componentWillUnmount()
在用户离开页面之前做任何事情。如果你使用的是功能组件,那么你可以用 useEffect()
钩子做同样的事情。钩子接受一个returns一个Destructor
的函数,这类似于componentWillUnmount()
可以做的
归功于 this article
这就是当用户切换到另一条路线或离开当前页面并转到另一条路线时显示消息的方式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
您的页面:
const [dirty, setDirty] = setState(false)
...
return (
<>
<LeavePageBlocker when={dirty} />
...
</>
)
我希望我的 ReactJS 应用在离开特定页面时通知用户。特别是提醒 him/her 执行操作的弹出消息:
"Changes are saved, but not published yet. Do that now?"
我应该在 react-router
全局触发这个,还是可以在 React 页面/组件中完成?
我还没有找到关于后者的任何信息,我宁愿避免使用第一个。除非它当然是常态,但这让我想知道如何在不必向用户可以访问的所有其他可能页面添加代码的情况下做这样的事情..
欢迎任何见解,谢谢!
对于 react-router
v0.13.x 和 react
v0.13.x:
这可以通过 willTransitionTo()
和 willTransitionFrom()
静态方法实现。对于较新的版本,请参阅下面我的其他答案。
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(); } } } } //... });
对于 react-router
1.0.0-rc1 with react
v0.14.x 或更高版本:
这应该可以通过 routerWillLeave
生命周期挂钩实现。对于旧版本,请参阅上面的回答。
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?' }, // ... })
东西。不过在最终版本发布之前可能会发生变化。
对于react-router
2.4.0+
注意:建议将所有代码迁移到最新的 react-router
以获得所有新功能。
react-router documentation中的推荐:
应该使用 withRouter
高阶组件:
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.
作为文档中的 ES6 示例:
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)
在react-router v2.4.0
或以上v4
之前有几个选项
<Route
path="/home"
onEnter={ auth }
onLeave={ showConfirm }
component={ Home }
>
您可以使用 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?'
},
// ...
})
)
请注意,此示例使用了 v2.4.0.
withRouter
高阶组件
然而,当手动更改 URL 中的路线时,这些解决方案并不能很好地工作
从某种意义上说
- 我们看到确认 - 好的
- 页面包含未重新加载 - 正常
- URL 没有变化 - 不行
对于 react-router v4
使用提示或自定义历史记录:
然而在 react-router v4
中,在 Prompt
from'react-router
根据文档
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.
在您的渲染方法中,您只需根据需要按照文档中的说明添加它。
更新:
如果您希望在用户离开页面时执行自定义操作,您可以使用自定义历史记录并像这样配置路由器
history.js
import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()
...
import { history } from 'path/to/history';
<Router history={history}>
<App/>
</Router>
然后在您的组件中,您可以使用 history.block
之类的
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
}
}
react-router
v4 引入了一种使用 Prompt
阻止导航的新方法。只需将其添加到您要阻止的组件中:
import { Prompt } from 'react-router'
const MyComponent = () => (
<>
<Prompt
when={shouldBlockNavigation}
message='You have unsaved changes, are you sure you want to leave?'
/>
{/* Component JSX */}
</>
)
这将阻止任何路由,但不会阻止页面刷新或关闭。要阻止它,您需要添加它(根据需要使用适当的 React 生命周期进行更新):
componentDidUpdate = () => {
if (shouldBlockNavigation) {
window.onbeforeunload = () => true
} else {
window.onbeforeunload = undefined
}
}
onbeforeunload 有各种浏览器支持。
对于react-router
v3.x
我遇到了同样的问题,我需要为页面上任何未保存的更改发送确认消息。在我的例子中,我使用的是 React Router v3,所以我不能使用 <Prompt />
,它是从 React Router v4 引入的。
我用 setRouteLeaveHook
和 history.pushState()
的组合处理了 'back button click' 和 'accidental link click',用 onbeforeunload
事件处理程序处理了 'reload button'。
setRouteLeaveHook (doc) & history.pushState (doc)
仅使用 setRouteLeaveHook 是不够的。出于某种原因,尽管单击 'back button' 时页面保持不变,但 URL 已更改。
// 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)
用于处理'accidental reload'按钮
window.onbeforeunload = this.handleOnBeforeUnload; ... handleOnBeforeUnload = e => { const message = 'Are you sure?'; e.returnValue = message; return message; }
以下是我编写的完整组件
- 请注意,withRouter 用于
this.props.router
。 - 请注意,
this.props.route
是从调用组件向下传递的 请注意,
currentState
作为 prop 传递以具有初始状态并检查任何更改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);
如果有任何问题,请告诉我:)
使用history.listen
例如如下:
在您的组件中,
componentWillMount() {
this.props.history.listen(() => {
// Detecting, user has changed URL
console.info(this.props.history.location.pathname);
});
}
你可以使用这个提示。
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;
也许您可以使用 componentWillUnmount()
在用户离开页面之前做任何事情。如果你使用的是功能组件,那么你可以用 useEffect()
钩子做同样的事情。钩子接受一个returns一个Destructor
的函数,这类似于componentWillUnmount()
可以做的
归功于 this article
这就是当用户切换到另一条路线或离开当前页面并转到另一条路线时显示消息的方式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
您的页面:
const [dirty, setDirty] = setState(false)
...
return (
<>
<LeavePageBlocker when={dirty} />
...
</>
)