用响应路由器检测用户离开页面

我希望我的 ReactJS 应用程序在离开特定页面时通知用户。特别是提醒他/她做某个动作的弹出消息:

“更改已保存,但尚未发布。现在可以这样做了吗?”

我应该在 react-router上全局触发它,还是可以在反应页面/组件中触发它?

我还没有找到任何关于后者的信息,我宁愿避开前者。当然,除非这是常态,但这让我想知道如何做到这一点,而不必向用户可以访问的每一个其他可能的页面添加代码。.

欢迎任何有见解的人,谢谢!

211319 次浏览

对于 react-router v0.13.x 和 react v0.13. x:

这是可能的与 willTransitionTo()willTransitionFrom()静态方法。对于较新的版本,见我的其他答案如下。

来自 反应路由器文档:

您可以在路由处理程序上定义一些在路由转换期间将被调用的静态方法。

willTransitionTo(transition, params, query, callback)

在处理程序即将呈现时调用,从而使您有机会中止或重定向转换。您可以在执行一些异步操作时暂停转换,并在完成时调用回调(错误) ,或者在参数列表中省略回调,它将为您调用。

willTransitionFrom(transition, component, callback)

当一个活动路由正在过渡时调用,给你一个中止过渡的机会。组件是当前组件,您可能需要它来检查其状态,以决定是否允许转换(如表单字段)。

例子

  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-router1.0.0-rc1和 react v0.14.x 或更高版本:

使用 routerWillLeave生命周期钩子应该可以做到这一点。

来自 反应路由器文档:

要安装这个钩子,请在路由组件中使用 LifCycle mix in。

  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-router2.4.0 +

注意 : 最好将所有代码迁移到最新的 react-router以获得所有新的好处。

根据 反应路由器文档的建议:

人们应该使用 withRouter高阶组件:

我们认为这个新的 HoC 更好,更容易,并将使用它在 文档和示例,但这并不是一个硬性要求 开关。

作为文档中的 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)

在反应路由器 v2.4.0或以上和 v4之前有几个选项

  1. Route添加函数 onLeave
 <Route
path="/home"
onEnter={ auth }
onLeave={ showConfirm }
component={ Home }
>
    

  1. componentDidMount使用函数 setRouteLeaveHook

您可以防止转换发生,或者在使用休假钩子离开路由之前提示用户。

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中,借助于 一个 href = “ https://reacttrain. com/response-router/web/api/Prompt”rel = “ noReferrer”> Prompt 的反应路由器实现起来相对容易些

根据文件记录

及时

用于在从页导航离开之前提示用户 应用程序进入一种状态,这种状态应该防止用户 导航离开(像一个表单是半填写) ,呈现一个 <Prompt>

import { Prompt } from 'react-router'


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

消息: 字符串

当用户试图导航离开时提示用户的消息。

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

信息: func

将与用户的下一个位置和操作一起调用 试图导航到。返回一个字符串以显示对 用户或真允许转换。

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

时间: 布尔

您不需要在警卫后面有条件地呈现 <Prompt>,而是 可以始终呈现它,但传递 when={true}when={false}到 相应地阻止或允许航行。

在呈现方法中,您只需根据需要添加文档中提到的内容。

更新:

如果您希望在用户离开页面时采取自定义操作,可以使用自定义历史记录并配置您的路由器

V 史

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
}
}

Onbefore 卸载 有各种浏览器的支持。

对于 react-router v3.x

我有同样的问题,我需要一个确认消息的任何未保存的更改在页面上。在我的例子中,我使用的是 反应路由器 v3,所以我不能使用从 反应路由器 v4引入的 <Prompt />

我用 setRouteLeaveHookhistory.pushState()的组合处理“后退按钮点击”和“偶然链接点击”,用 onbeforeunload事件处理程序处理“重新加载按钮”。

SetRouteLeaveHook (医生) & 历史,推国家(医生)

  • 仅仅使用 setRouteLeaveHook 是不够的。由于某种原因,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)

  • 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 :)

利用历史,听着

例如:

在您的组件中,

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()钩子进行同样的操作。钩子接受一个返回 Destructor的函数,这与 componentWillUnmount()的功能类似。

这要归功于 这篇文章

这样,当用户切换到另一个路由或离开当前页面转到另一个 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} />
...
</>
)

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()
}
},[])

对于反应路由器 v3.x 和功能组件,我们可以使用这样的钩子:

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 };

然后,在另一个文件中,我们在保存数据后禁用确认功能:

  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);
});

这里的 ofc 信息是关于“ use粘贴”挂钩的: 如何比较旧值和新值的反应钩使用效果?