使用React-Router检测用户离开页面


115

我希望我的ReactJS应用在离开特定页面时通知用户。特别是一条弹出消息,提醒他/她进行以下操作:

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

我应该在react-router全局范围内触发此操作,还是可以在react页面/组件内完成此操作?

我还没有发现任何关于后者的信息,我宁愿避免使用第一个。除非当然是其规范,否则这使我想知道如何执行这样的事情而不必将代码添加到用户可以访问的所有其他可能的页面中。

欢迎有任何见解,谢谢!


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

Answers:


153

react-routerv4引入了一种使用阻止导航的新方法Prompt。只需将其添加到您要阻止的组件中即可:

import { Prompt } from 'react-router'

const MyComponent = () => (
  <React.Fragment>
    <Prompt
      when={shouldBlockNavigation}
      message='You have unsaved changes, are you sure you want to leave?'
    />
    {/* Component JSX */}
  </React.Fragment>
)

这将阻止所有路由,但不会阻止页面刷新或关闭。要阻止它,您需要添加以下内容(根据需要使用相应的React生命周期进行更新):

componentDidUpdate = () => {
  if (shouldBlockNavigation) {
    window.onbeforeunload = () => true
  } else {
    window.onbeforeunload = undefined
  }
}

onbeforeunload具有浏览器的各种支持。


9
但是,这将导致两个外观迥然不同的警报。
XanderStrike

3
@XanderStrike您可以尝试设置提示警报的样式,以模仿浏览器的默认警报。不幸的是,无法设置onberforeunload警报的样式。
jcady

例如,一旦用户单击CANCEL按钮,是否可以显示相同的Prompt组件?
Rene Enriquez

1
@ReneEnriquez react-router不支持该功能(假设“取消”按钮不会触发任何路线更改)。您可以创建自己的模态来模仿行为。
jcady

6
如果最终使用onbeforeunload ,则需要在卸载组件时对其进行清理。 componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch

40

在React-Router v2.4.0或更高版本中以及之前v4有几种选择

  1. 附加功能onLeaveRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. 使用功能setRouteLeaveHookcomponentDidMount

您可以防止发生过渡,也可以在离开带有离开钩子的路线之前提示用户。

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

    // ...

  })
)

请注意,这个例子使用了的withRouter在推出高阶组件v2.4.0.

但是,当手动更改URL中的路由时,这些解决方案并不十分理想

在某种意义上说

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

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

但是,在from'react-router react-router v4的帮助下,它更容易实现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?"/>

讯息:功能

将会与用户尝试导航到的下一个位置和操作一起调用。返回一个字符串以向用户显示提示,或者返回true以允许过渡。

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

时间:布尔

<Prompt>您可以始终渲染它,而可以通过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.blocklike

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

1
即使您使用<Prompt>URL,当在提示上按“取消”时,URL 也会更改。相关的问题:github.com/ReactTraining/react-router/issues/5405
萨汉Serasinghe

1
我看到这个问题仍然很流行。由于最重要的答案是关于旧版本的,因此我将这个较新的答案设置为可接受的答案。现在可以在github.com/ReactTraining/react-router上
Barry Staes

1
确认路线更改后会触发onLeave。您能否详细说明如何取消onLeave中的导航?
schlingel

23

对于react-router2.4.0+

注意:建议将所有代码迁移到最新版本,react-router以获取所有新功能。

按照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)

4
RouteLeaveHook回调有什么作用?它会提示用户使用内置模式吗?如果您想要自定义模态怎么办
Learner

就像@Learner:如何使用自定义的确认框(例如vex或SweetAlert或其他非阻塞对话框)来做到这一点?
olefrank

我遇到以下错误:-TypeError:无法读取未定义的属性“ id ”。任何建议
Mustafa Mamun

@MustafaMamun看来这是一个不相关的问题,您通常会想创建一个新问题来解释细节。
snymkpr

尝试使用解决方案时遇到问题。该组件必须直接连接到路由器,然后此解决方案才能起作用。我发现了一个更好的回答有stackoverflow.com/questions/39103684/...
穆斯塔法·马蒙

4

对于react-routerv3.x

我遇到了同样的问题,该页面上任何未保存的更改都需要确认消息。就我而言,我使用的是React Router v3,因此无法使用<Prompt />,它是从React Router v4引入的。

我处理“后退按钮点击”和“意外的链接点击”用的组合setRouteLeaveHookhistory.pushState(),并办理“刷新按钮”与onbeforeunload事件处理程序。

setRouteLeaveHookdoc)和history.pushStatedoc

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

onbeforeunloaddoc

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

    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到底是哪里来的?
加拉夫

2

对于react-routerv0.13.x和reactv0.13.x:

使用willTransitionTo()willTransitionFrom()静态方法可以做到这一点。对于较新的版本,请参见下面的其他答案。

react-router文档中

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

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

    //...
  });

对于v0.14.x或更高版本的react-router1.0.0-rc1 react

使用routerWillLeave生命周期钩子应该可以做到这一点。对于较旧的版本,请参见上面的答案。

react-router文档中

要安装此挂钩,请在一个路由组件中使用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?'
    },

    // ...

  })

东西。可能会在最终版本之前更改。


1

您可以使用此提示。

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;

1

使用history.listen

例如下面的例子:

在您的组件中,

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

5
嗨,欢迎来到Stack Overflow。当回答已经有很多答案的问题时,请确保对您提供的回答为何具有实质性,而不是简单地呼应原始张贴者已经审查过的内容,增加一些其他见解。这在“仅代码”答案(例如您提供的答案)中尤其重要。
chb
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.