【问题标题】:Detecting user leaving page with react-router使用 react-router 检测用户离开页面
【发布时间】:2015-12-26 19:16:09
【问题描述】:

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

“更改已保存,但尚未发布。现在执行吗?”

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

我没有找到关于后者的任何内容,我宁愿避免使用第一个。当然,除非它是常态,但这让我想知道如何在不必向用户可以访问的每个其他可能页面添加代码的情况下做这样的事情..

欢迎任何见解,谢谢!

【问题讨论】:

  • 我现在不知道这是否是您要搜索的内容,但是您可以做某事。像这样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 } } 这样你就可以在离开页面之前调用你的发布函数

标签: reactjs react-router


【解决方案1】:

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

这可以通过willTransitionTo()willTransitionFrom() 静态方法实现。对于较新的版本,请参阅下面的我的其他答案。

来自react-router documentation

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

willTransitionTo(transition, params, query, callback)

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

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

这应该可以通过 routerWillLeave 生命周期钩子实现。对于旧版本,请参阅我上面的答案。

来自react-router documentation

要安装此挂钩,请在您的一个路由组件中使用 Lifecycle mixin。

  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?'
    },

    // ...

  })

事情。不过在最终发布之前可能会发生变化。

【讨论】:

    【解决方案2】:

    对于react-router 2.4.0+

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

    react-router documentation中推荐的:

    应该使用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)
    

    【讨论】:

    • RouteLeaveHook 回调有什么作用?它是否使用内置模式提示用户?如果你想要一个自定义模态怎么办
    • 点赞@Learner:如何使用自定义确认框(如 vex 或 SweetAlert 或其他非阻塞对话框)来做到这一点?
    • 我遇到以下错误:- TypeError:无法读取未定义的属性“id”。任何建议
    • @MustafaMamun 这似乎是一个不相关的问题,您最想创建一个新问题来解释细节。
    • 我在尝试使用该解决方案时遇到了问题。该组件必须直接连接到路由器,然后此解决方案才有效。我在那里找到了更好的答案stackoverflow.com/questions/39103684/…
    【解决方案3】:

    在 react-router v2.4.0 或以上以及v4 之前有几个选项

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

    您可以在使用离开钩子离开路线之前阻止转换发生或提示用户。

    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 中的路由时,这些解决方案并不完美

    从某种意义上说

    • 我们看到了确认 - 好的
    • 包含的页面不会重新加载 - 好的
    • 网址没有变化 - 不行

    对于使用提示或自定义历史记录的 react-router v4

    但是在 react-router v4 中,在 Prompt from'react-router

    的帮助下实现起来相当容易

    根据文档

    提示

    用于在离开页面之前提示用户。当你的 应用程序进入一个状态,应该阻止用户 导航离开(就像一个表格填了一半),渲染一个&lt;Prompt&gt;

    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?"/>
    

    消息:函数

    将调用用户的下一个位置和操作 试图导航到。返回一个字符串以向 user 或 true 以允许转换。

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

    何时:布尔

    不是有条件地在守卫后面渲染&lt;Prompt&gt;,而是 始终可以渲染它,但将 when={true}when={false} 传递给 相应地阻止或允许导航。

    在您的渲染方法中,您只需要根据您的需要按照文档中的说明添加它即可。

    更新:

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

    history.js

    import createBrowserHistory from 'history/createBrowserHistory'
    export const history = createBrowserHistory()
    
    ... 
    import { history } from 'path/to/history';
    <Router history={history}>
      <App/>
    </Router>
    

    然后在你的组件中你可以使用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
       }
    }
    

    【讨论】:

    • 即使您使用&lt;Prompt&gt;,当您在提示符上按取消时,URL 也会发生变化。相关问题:github.com/ReactTraining/react-router/issues/5405
    • 我看这个问题还是挺热门的。由于最佳答案是针对旧的已弃用版本,因此我将这个较新的答案设置为已接受的答案。旧文档(和升级指南)的链接现在可以在 github.com/ReactTraining/react-router 获得
    • onLeave 在确认路线更改后被触发。能否详细说明如何取消 onLeave 中的导航?
    • history.block() 是运行任何“路由前”逻辑的好方法,例如存储滚动位置。
    【解决方案4】:

    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有多种浏览器支持。

    【讨论】:

    • 这会导致两个外观截然不同的警报。
    • @XanderStrike 您可以尝试设置提示警报的样式以模仿浏览器默认警报。不幸的是,无法设置 onberforeunload 警报的样式。
    • @ReneEnriquez react-router 不支持开箱即用(假设取消按钮不会触发任何路由更改)。不过,您可以创建自己的模态来模仿行为。
    • 如果您最终使用onbeforeunload ,您需要在卸载组件时清理它。 componentWillUnmount() { window.onbeforeunload = null; }
    • 片段可以缩短为&lt;&gt;&lt;/&gt;
    【解决方案5】:

    对于react-router v3.x

    我遇到了同样的问题,我需要确认页面上任何未保存的更改的消息。就我而言,我使用的是 React Router v3,所以我无法使用从 React Router v4 引入的&lt;Prompt /&gt;

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

    setRouteLeaveHook (doc) & history.pushState (doc)

    • 仅使用 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)

    • 用于处理“意外重载”按钮

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

    如果有任何问题请告诉我:)

    【讨论】:

    • this.formStartEle 究竟是从哪里来的?
    • 谢谢! window.history.pushState() 的技巧真的很有帮助
    【解决方案6】:

    使用 history.listen

    例如如下:

    在您的组件中,

    componentWillMount() {
        this.props.history.listen(() => {
          // Detecting, user has changed URL
          console.info(this.props.history.location.pathname);
        });
    }
    

    【讨论】:

    • 嗨,欢迎来到 Stack Overflow。在回答已经有很多答案的问题时,请务必添加一些额外的见解,说明为什么您提供的回复是实质性的,而不是简单地呼应原始发帖人已经审查过的内容。这在您提供的“纯代码”答案中尤其重要。
    【解决方案7】:

    您可以使用此提示。

    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;
    

    【讨论】:

      【解决方案8】:

      也许您可以在用户离开页面之前使用componentWillUnmount() 做任何事情。如果您使用的是功能组件,那么您可以使用 useEffect() 钩子来做同样的事情。该钩子接受一个返回Destructor 的函数,这类似于componentWillUnmount() 可以做的事情。

      归功于this article

      【讨论】:

        【解决方案9】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-11-17
          • 1970-01-01
          • 1970-01-01
          • 2012-06-17
          • 2019-04-01
          • 2018-03-19
          相关资源
          最近更新 更多