反応ルーターを使用してユーザーがページを離れるのを検出する


115

ReactJSアプリで、特定のページから移動したときにユーザーに通知する必要があります。具体的には、アクションを実行するように促すポップアップメッセージ:

「変更は保存されていますが、まだ公開されていません。今すぐ実行しますか?」

これをreact-routerグローバルにトリガーする必要がありますか、これは反応ページ/コンポーネント内から実行できるものですか?

後者については何も発見していないので、最初のものは避けたい。もちろん、それが標準でない限り、ユーザーがアクセスできる他のすべての可能なページにコードを追加する必要なしに、そのようなことを行う方法を疑問に思います。

どんな洞察も歓迎します、ありがとう!


3
これがあなたが探しているものではないのですが、あなたはsthを実行できます。この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

回答:


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
これにより、2つの非常に異なる外観のアラートが生成されます。
XanderStrike

3
@XanderStrikeブラウザーのデフォルトのアラートを模倣するようにプロンプ​​トアラートのスタイルを設定することができます。残念ながら、onberforeunloadアラートのスタイルを設定する方法はありません。
jcady

たとえば、ユーザーが[キャンセル]ボタンをクリックしたときに同じプロンプトコンポーネントを表示する方法はありますか?
ルネエンリケス2018

1
@ReneEnriquez react-routerはそのままではサポートしていません(キャンセルボタンがルートの変更をトリガーしない場合)。ただし、動作を模倣する独自のモーダルを作成できます。
jcady 2018年

6
最終的にを使用onbeforeunload する場合は、コンポーネントがマウント解除されたときにクリーンアップする必要があります。 componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch

40

反応ルーターv2.4.0以上で、v4いくつかのオプションがある前に

  1. 関数onLeaveを追加Route
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. 関数setRouteLeaveHookを使用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?'
    },

    // ...

  })
)

この例では、でwithRouter導入された高次コンポーネントを使用していることに注意してくださいv2.4.0.

ただし、これらのソリューションは、URLのルートを手動で変更すると完全に機能しません

という意味で

  • 確認が表示されます-わかりました
  • ページの包含がリロードしない-わかりました
  • URLは変更されません-正常ではありません

以下のためのreact-router v4プロンプトまたはカスタムの歴史を使用しました:

ただし、react-router v4では、Promptfrom'react-router を使用して実装する方がかなり簡単です。

ドキュメントによると

促す

ページから移動する前にユーザーにプロンプ​​トを表示するために使用されます。アプリケーションが、ユーザーがナビゲートできないような状態(フォームが半分埋められているなど)になったら、をレンダリングし<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

ユーザーがナビゲートしようとしている次の場所とアクションで呼び出されます。ユーザーにプロンプ​​トを表示する文字列、または遷移を許可する場合は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.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
   }
}

1
<Prompt>プロンプトで[キャンセル]を押すと、URL を使用しても変更されます。関連問題:github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe

1
この質問はまだかなり人気があります。上位の回答は古い非推奨バージョンに関するものであるため、この新しい回答を承認済みの回答として設定します。古いドキュメント(およびアップグレードガイド)へのリンクがgithub.com/ReactTraining/react-routerで
Barry Staes

1
onLeaveは、ルート変更が確認された後に発生します。onLeaveでナビゲーションをキャンセルする方法について詳しく教えてください。
シュリンゲル2018

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 2017年

次のエラーが発生しています:-TypeError:未定義のプロパティ ' id 'を読み取れません。任意の提案
Mustafa Mamun 2017年

@MustafaMamunそれは無関係な問題のようです、そしてあなたは主に詳細を説明する新しい質問を作成したいと思うでしょう。
snymkpr 2017年

ソリューションを使用しようとしたときに問題が発生しました。コンポーネントはルーターに直接接続する必要があり、このソリューションが機能します。私はそこに、より良い答えが見つからstackoverflow.com/questions/39103684/...
ムスタファMamun

4

以下のためのreact-routerバージョン3.x

ページで保存されていない変更について確認メッセージが必要なのと同じ問題がありました。私の場合、React Router v3を使用していたため<Prompt />React Router v4から導入されたを使用できませんでした。

私はの組み合わせで「戻るボタンをクリック」と「偶然のリンクのクリックを」扱いsetRouteLeaveHookhistory.pushState()し、で「リロードボタン」を取り扱うonbeforeunloadイベントハンドラ。

setRouteLeaveHookドキュメント)&history.pushStateドキュメント

  • 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ドキュメント

  • 「偶発的なリロード」ボタンを処理するために使用されます

    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はどこから来ているのですか?
Gaurav

2

react-routerとv0.13.x reactv0.13.x:

これはwillTransitionTo()およびwillTransitionFrom()メソッドで可能です。新しいバージョンについては、以下の他の回答を参照してください。

反応し、ルーターのマニュアル

ルート遷移中に呼び出されるいくつかの静的メソッドをルートハンドラーに定義できます。

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はreactv0.14.x以降:

これはrouterWillLeaveライフサイクルフックで可能です。古いバージョンについては、上記の私の回答を参照してください。

反応し、ルーターのマニュアル

このフックをインストールするには、ルートコンポーネントの1つでライフサイクルミックスインを使用します。

  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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.