Mendeteksi halaman keluar pengguna dengan react-router

117

Saya ingin aplikasi ReactJS saya memberi tahu pengguna saat keluar dari halaman tertentu. Khususnya pesan popup yang mengingatkan dia untuk melakukan suatu tindakan:

"Perubahan disimpan, tapi belum dipublikasikan. Lakukan sekarang?"

Haruskah saya memicu ini react-routersecara global, atau apakah ini sesuatu yang dapat dilakukan dari dalam halaman / komponen react?

Saya belum menemukan apa pun tentang yang terakhir, dan saya lebih suka menghindari yang pertama. Kecuali itu adalah norma tentu saja, tetapi itu membuat saya bertanya-tanya bagaimana melakukan hal seperti itu tanpa harus menambahkan kode ke setiap halaman lain yang memungkinkan pengguna dapat pergi ..

Semua wawasan diterima, terima kasih!

Barry Staes
sumber
3
Saya tidak sekarang jika ini yang Anda cari, tetapi Anda dapat melakukan sth. seperti ini 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 } }sehingga Anda dapat memanggil fungsi publikasikan sebelum meninggalkan laman
Rico Berger

Jawaban:

154

react-routerv4 memperkenalkan cara baru untuk memblokir navigasi menggunakan Prompt. Cukup tambahkan ini ke komponen yang ingin Anda blokir:

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

Ini akan memblokir perutean apa pun, tetapi bukan penyegaran atau penutupan halaman. Untuk memblokirnya, Anda harus menambahkan ini (memperbarui sesuai kebutuhan dengan siklus hidup React yang sesuai):

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

onbeforeunload memiliki berbagai dukungan oleh browser.

jcady.dll
sumber
9
Ini menghasilkan dua peringatan yang tampak sangat berbeda.
XanderStrike
3
@XanderStrike Anda dapat mencoba mengatur gaya peringatan prompt untuk meniru peringatan default browser. Sayangnya, tidak ada cara untuk mengatur onberforeunloadperingatan.
jcady
Apakah ada cara untuk menampilkan komponen Prompt yang sama setelah pengguna mengklik tombol BATAL misalnya?
Rene Enriquez
1
@ReneEnriquez react-routertidak mendukungnya di luar kotak (dengan asumsi tombol batal tidak memicu perubahan rute apa pun). Anda dapat membuat modal Anda sendiri untuk meniru perilaku tersebut.
jcady
6
Jika Anda akhirnya menggunakan onbeforeunload , Anda pasti ingin membersihkannya saat komponen Anda dilepas. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch
40

Di react-router v2.4.0atau di atasnya dan sebelumnya v4ada beberapa opsi

  1. Tambahkan fungsi onLeaveuntukRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Gunakan fungsi setRouteLeaveHookuntukcomponentDidMount

Anda dapat mencegah transisi terjadi atau meminta pengguna sebelum meninggalkan rute dengan hook tinggalkan.

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

    // ...

  })
)

Perhatikan bahwa contoh ini memanfaatkan withRouterkomponen tingkat tinggi yang diperkenalkan div2.4.0.

Namun solusi ini tidak bekerja dengan sempurna saat mengubah rute di URL secara manual

Dalam artian itu

  • kita melihat Konfirmasi - ok
  • berisi halaman tidak dimuat ulang - oke
  • URL tidak berubah - tidak oke

Untuk react-router v4menggunakan riwayat Prompt atau kustom:

Namun react-router v4, lebih mudah untuk diimplementasikan dengan bantuan Promptfrom'react-router

Menurut dokumentasi

Cepat

Digunakan untuk meminta pengguna sebelum keluar dari halaman. Saat aplikasi Anda memasuki keadaan yang seharusnya mencegah pengguna keluar (seperti formulir yang setengah terisi), render a <Prompt>.

import { Prompt } from 'react-router'

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

pesan: string

Pesan untuk meminta pengguna saat mereka mencoba keluar.

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

pesan: func

Akan dipanggil dengan lokasi dan tindakan berikutnya yang coba dinavigasi oleh pengguna. Kembalikan string untuk menampilkan prompt kepada pengguna atau true untuk mengizinkan transisi.

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

kapan: bool

Alih-alih merender secara kondisional di <Prompt>belakang pelindung, Anda selalu dapat merendernya tetapi melewati when={true}atau when={false}untuk mencegah atau mengizinkan navigasi yang sesuai.

Dalam metode render Anda, Anda hanya perlu menambahkan ini seperti yang disebutkan dalam dokumentasi sesuai kebutuhan Anda.

MEMPERBARUI:

Jika Anda ingin memiliki tindakan khusus yang harus diambil saat menggunakan meninggalkan halaman, Anda dapat menggunakan riwayat khusus dan mengkonfigurasi Router Anda seperti

history.js

import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()

... 
import { history } from 'path/to/history';
<Router history={history}>
  <App/>
</Router>

dan kemudian di komponen Anda, Anda dapat menggunakan history.blocksuka

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
   }
}
Shubham Khatri
sumber
1
Bahkan jika Anda menggunakan <Prompt>URL akan berubah saat Anda menekan Batal saat diminta. Masalah terkait: github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe
1
Saya melihat pertanyaan ini masih cukup populer. Karena jawaban teratas adalah tentang untuk versi lama yang tidak digunakan lagi, saya menetapkan jawaban yang lebih baru ini sebagai jawaban yang diterima. Tautan ke dokumentasi lama (dan panduan peningkatan) sekarang tersedia di github.com/ReactTraining/react-router
Barry Staes
1
onLeave diaktifkan SETELAH perubahan rute dikonfirmasi. Bisakah Anda menjelaskan tentang cara membatalkan navigasi di onLeave?
schlingel
23

Untuk react-router2.4.0+

CATATAN : Dianjurkan untuk memigrasi semua kode Anda ke yang terbaru react-routeruntuk mendapatkan semua barang baru.

Seperti yang direkomendasikan dalam dokumentasi react-router :

Seseorang harus menggunakan withRouterkomponen urutan yang lebih tinggi:

Menurut kami HoC baru ini lebih bagus dan lebih mudah, dan akan digunakan dalam dokumentasi dan contoh, tetapi ini bukan persyaratan yang sulit untuk beralih.

Sebagai contoh ES6 dari dokumentasi:

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)
snymkpr
sumber
4
Apa yang dilakukan callback RouteLeaveHook? Apakah itu meminta pengguna menggunakan modal bawaan? Bagaimana jika Anda menginginkan modal khusus
Pelajar
Seperti @Pelajar: Bagaimana melakukan ini dengan kotak konfirmasi yang disesuaikan seperti vex atau SweetAlert atau dialog non-pemblokiran lainnya?
olefrank
Saya mengalami kesalahan berikut: - TypeError: Tidak dapat membaca properti ' id ' dari tidak ditentukan. Ada saran
Mustafa Mamun
@MustafaMamun Itu tampaknya masalah yang tidak terkait dan Anda sebagian besar ingin membuat pertanyaan baru yang menjelaskan detailnya.
snymkpr
Saya mengalami masalah saat mencoba menggunakan solusinya. Komponen harus terhubung langsung ke router agar solusi ini berfungsi. Saya menemukan jawaban yang lebih baik di sana stackoverflow.com/questions/39103684/…
Mustafa Mamun
4

Untuk react-routerv3.x

Saya memiliki masalah yang sama ketika saya membutuhkan pesan konfirmasi untuk setiap perubahan yang belum disimpan di halaman. Dalam kasus saya, saya menggunakan React Router v3 , jadi saya tidak dapat menggunakan <Prompt />, yang diperkenalkan dari React Router v4 .

Saya menangani 'klik tombol kembali' dan 'klik tautan tidak disengaja' dengan kombinasi setRouteLeaveHookdan history.pushState(), dan menangani 'tombol muat ulang' dengan onbeforeunloadpengendali kejadian.

setRouteLeaveHook ( doc ) & history.pushState ( doc )

  • Hanya menggunakan setRouteLeaveHook saja tidak cukup. Untuk beberapa alasan, URL diubah meskipun halaman tetap sama ketika 'tombol kembali' diklik.

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

  • Ini digunakan untuk menangani tombol 'muat ulang tidak disengaja'

    window.onbeforeunload = this.handleOnBeforeUnload;
    
    ...
    
    handleOnBeforeUnload = e => {
      const message = 'Are you sure?';
      e.returnValue = message;
      return message;
    }

Di bawah ini adalah komponen lengkap yang telah saya tulis

  • perhatikan bahwa withRouter digunakan untuk memiliki this.props.router.
  • catatan yang this.props.routediturunkan dari komponen pemanggil
  • catatan yang currentStatedilewatkan sebagai prop untuk memiliki status awal dan untuk memeriksa setiap perubahan

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

Tolong beri tahu saya jika ada pertanyaan :)

Taman Sang Yun
sumber
dari mana sebenarnya this.formStartEle berasal?
Gaurav
2

Untuk react-routerv0.13.x dengan reactv0.13.x:

ini dimungkinkan dengan metode willTransitionTo()dan willTransitionFrom()statis. Untuk versi yang lebih baru, lihat jawaban saya yang lain di bawah ini.

Dari dokumentasi react-router :

Anda dapat menentukan beberapa metode statis pada penangan rute Anda yang akan dipanggil selama transisi rute.

willTransitionTo(transition, params, query, callback)

Dipanggil saat penangan akan merender, memberi Anda kesempatan untuk membatalkan atau mengalihkan transisi. Anda dapat menjeda transisi saat melakukan pekerjaan asinkron dan memanggil callback (error) setelah selesai, atau menghilangkan callback dalam daftar argumen Anda dan callback akan dipanggil untuk Anda.

willTransitionFrom(transition, component, callback)

Dipanggil ketika rute aktif sedang dialihkan memberi Anda kesempatan untuk membatalkan transisi. Komponennya adalah komponen saat ini, Anda mungkin memerlukannya untuk memeriksa statusnya untuk memutuskan apakah Anda ingin mengizinkan transisi (seperti kolom formulir).

Contoh

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

    //...
  });

Untuk react-router1.0.0-rc1 dengan reactv0.14.x atau yang lebih baru:

ini harus dimungkinkan dengan routerWillLeavepengait siklus hidup. Untuk versi yang lebih lama, lihat jawaban saya di atas.

Dari dokumentasi react-router :

Untuk memasang hook ini, gunakan mixin Lifecycle di salah satu komponen rute Anda.

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

    // ...

  })

Sesuatu. mungkin berubah sebelum rilis final.

Barry Staes
sumber
1

Anda dapat menggunakan prompt ini.

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;
Jaskaran Singh
sumber
1

Menggunakan history.listen

Contohnya seperti di bawah ini:

Di komponen Anda,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Debabrata Ghosh
sumber
5
Hai, selamat datang di Stack Overflow. Saat menjawab pertanyaan yang sudah memiliki banyak jawaban, pastikan untuk menambahkan beberapa wawasan tambahan tentang mengapa tanggapan yang Anda berikan bersifat substantif dan tidak hanya menggemakan apa yang telah diperiksa oleh pembuat poster asli. Ini terutama penting dalam jawaban "hanya kode" seperti yang Anda berikan.
chb