Wykrywanie opuszczania strony przez użytkownika za pomocą routera reaktywnego

118

Chcę, aby moja aplikacja ReactJS powiadamiała użytkownika o opuszczeniu określonej strony. W szczególności wyskakująca wiadomość przypominająca mu o wykonaniu czynności:

„Zmiany zostały zapisane, ale nie zostały jeszcze opublikowane. Zrobić to teraz?”

Czy powinienem wywołać to react-routerglobalnie, czy jest to coś, co można zrobić z poziomu strony / komponentu reagowania?

Nie znalazłem nic na temat tego ostatniego i wolałbym unikać pierwszego. Chyba że jest to norma, ale to sprawia, że ​​zastanawiam się, jak zrobić coś takiego bez konieczności dodawania kodu do każdej innej możliwej strony, do której użytkownik może przejść.

Wszelkie spostrzeżenia mile widziane, dzięki!

Barry Staes
źródło
3
Nie wiem, jeśli tego właśnie szukasz, ale możesz coś zrobić. w ten 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 } }sposób, aby móc wywołać funkcję publikowania przed opuszczeniem strony
Rico Berger

Odpowiedzi:

155

react-routerv4 wprowadza nowy sposób blokowania nawigacji przy użyciu Prompt. Po prostu dodaj to do komponentu, który chcesz zablokować:

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

Spowoduje to zablokowanie routingu, ale nie będzie odświeżania ani zamykania strony. Aby to zablokować, musisz to dodać (aktualizując w razie potrzeby z odpowiednim cyklem życia React):

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

onbeforeunload obsługuje różne przeglądarki.

jcady
źródło
9
Powoduje to jednak dwa bardzo różnie wyglądające alerty.
XanderStrike
3
@XanderStrike Możesz spróbować stylizować alert zachęty, aby naśladować domyślny alert przeglądarki. Niestety nie ma sposobu, aby stylizować onberforeunloadalert.
jcady
Czy istnieje sposób, aby wyświetlić ten sam komponent Monit, na przykład po kliknięciu przez użytkownika przycisku ANULUJ?
Rene Enriquez,
1
@ReneEnriquez react-routernie obsługuje tego po wyjęciu z pudełka (zakładając, że przycisk anulowania nie powoduje żadnych zmian trasy). Możesz jednak stworzyć własny modal, aby naśladować to zachowanie.
jcady
6
Jeśli skończysz z używaniem onbeforeunload , będziesz chciał go wyczyścić po odłączeniu komponentu. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch
40

W React-Router v2.4.0lub wyżej i wcześniej v4jest kilka opcji

  1. Dodaj funkcję onLeavedlaRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Użyj funkcji setRouteLeaveHookdlacomponentDidMount

Możesz zapobiec wystąpieniu przejścia lub monitować użytkownika przed opuszczeniem trasy za pomocą haka opuszczenia.

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

    // ...

  })
)

Należy zauważyć, że w tym przykładzie wykorzystano składnik withRouterwyższego rzędu wprowadzony wv2.4.0.

Jednak to rozwiązanie nie działa idealnie przy ręcznej zmianie trasy w adresie URL

W tym sensie

  • widzimy Potwierdzenie - ok
  • zawartość strony nie ładuje się ponownie - ok
  • URL się nie zmienia - nie w porządku

Aby react-router v4użyć Monit lub historii niestandardowej:

Jednak react-router v4jest to raczej łatwiejsze do wdrożenia przy pomocyPrompt speak-router

Zgodnie z dokumentacją

Skłonić

Służy do monitowania użytkownika przed opuszczeniem strony. Gdy aplikacja wejdzie w stan, który powinien uniemożliwić użytkownikowi opuszczenie go (np. Formularz jest wypełniony do połowy), wyrenderuj plik <Prompt>.

import { Prompt } from 'react-router'

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

wiadomość: ciąg

Komunikat monitujący użytkownika, gdy próbuje opuścić stronę.

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

wiadomość: func

Zostanie wywołana z następną lokalizacją i czynnością, do której użytkownik próbuje przejść. Zwróć ciąg, aby wyświetlić monit dla użytkownika, lub true, aby zezwolić na przejście.

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

kiedy: bool

Zamiast warunkowego renderowania znaku <Prompt>za strażnikiem, zawsze możesz go wyrenderować, ale przejść when={true}lub when={false}odpowiednio uniemożliwić lub zezwolić na nawigację.

W swojej metodzie renderowania wystarczy dodać to, jak wspomniano w dokumentacji, zgodnie z potrzebami.

AKTUALIZACJA:

W przypadku, gdy chciałbyś mieć niestandardową akcję do wykonania, gdy używasz opuszczania strony, możesz skorzystać z niestandardowej historii i skonfigurować router jak

history.js

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

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

a następnie w swoim komponencie możesz wykorzystać history.blockpodobne

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
źródło
1
Nawet jeśli używasz <Prompt>adresu URL, zostanie zmieniony po naciśnięciu przycisku Anuluj w monicie. Powiązany problem: github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe
1
Widzę, że to pytanie jest nadal dość popularne. Ponieważ najpopularniejsze odpowiedzi dotyczą starszych, przestarzałych wersji, ustawiłem nowszą odpowiedź jako zaakceptowaną. Linki do starej dokumentacji (i przewodników aktualizacji) są teraz dostępne na github.com/ReactTraining/react-router
Barry Staes
1
onLeave jest uruchamiany PO potwierdzeniu zmiany trasy. Czy możesz wyjaśnić, jak anulować nawigację w onLeave?
schlingel
23

Dla react-router2.4.0+

UWAGA : Zaleca się migrację całego kodu do najnowszejreact-router aby uzyskać wszystkie nowe gadżety.

Zgodnie z zaleceniami w dokumentacji reaktora routera :

Należy użyć withRouterkomponentu wyższego rzędu:

Uważamy, że ten nowy HoC jest ładniejszy i łatwiejszy i będziemy go używać w dokumentacji i przykładach, ale zmiana nie jest trudna.

Jako przykład ES6 z dokumentacji:

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
źródło
4
Co robi wywołanie zwrotne RouteLeaveHook? Czy zachęca użytkownika do korzystania z wbudowanego modalu? A co, jeśli chcesz niestandardowego modalu
Learner
Podobnie jak @Learner: Jak to zrobić za pomocą dostosowanego okna potwierdzenia, takiego jak vex lub SweetAlert lub innego nieblokującego okna dialogowego?
olefrank
Mam następujący błąd: - TypeError: Nie można odczytać właściwości „ id ” niezdefiniowanej. Wszelkie sugestie
Mustafa Mamun
@MustafaMamun To wydaje się być niepowiązanym problemem i najczęściej będziesz chciał utworzyć nowe pytanie wyjaśniające szczegóły.
snymkpr
Miałem problem podczas próby użycia rozwiązania. Komponent musi być bezpośrednio podłączony do routera, to rozwiązanie działa. Znalazłem tam lepszą odpowiedź stackoverflow.com/questions/39103684/…
Mustafa Mamun
4

W wersji react-router3.x

Miałem ten sam problem, w którym potrzebowałem wiadomości z potwierdzeniem każdej niezapisanej zmiany na stronie. W moim przypadku korzystałem z React Router v3 , więc nie mogłem używać <Prompt />, który został wprowadzony z React Router v4 .

Obsłużyłem „kliknięcie przycisku wstecz” i „przypadkowe kliknięcie linku” za pomocą kombinacji setRouteLeaveHooki history.pushState(), a „przycisk przeładowania” obsłużyłem za pomocąonbeforeunload obsługą zdarzeń.

setRouteLeaveHook ( doc ) & history.pushState ( doc )

  • Używanie tylko setRouteLeaveHook nie wystarczyło. Z jakiegoś powodu adres URL został zmieniony, chociaż strona pozostała taka sama po kliknięciu przycisku „Wstecz”.

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

  • Służy do obsługi przycisku „przypadkowego przeładowania”

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

Poniżej znajduje się pełny komponent, który napisałem

  • zwróć uwagę, że withRouter jest używany dothis.props.router .
  • uwaga, która this.props.routejest przekazywana z komponentu wywołującego
  • zauważ, że currentStatejest przekazywany jako prop, aby mieć stan początkowy i sprawdzić wszelkie zmiany

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

Daj mi znać, jeśli masz jakieś pytania :)

Sang Yun Park
źródło
skąd dokładnie pochodzi this.formStartEle?
Gaurav
2

Dla react-routerv0.13.x z reactv0.13.x:

jest to możliwe dzięki metodom willTransitionTo()i willTransitionFrom()statycznym. W przypadku nowszych wersji zobacz moją inną odpowiedź poniżej.

Z dokumentacji reaktora routera :

W programach obsługi tras można zdefiniować metody statyczne, które będą wywoływane podczas przejść tras.

willTransitionTo(transition, params, query, callback)

Wywoływana, gdy program obsługi ma zamiar wyrenderować, co daje możliwość przerwania lub przekierowania przejścia. Możesz wstrzymać przejście, gdy wykonujesz jakąś asynchroniczną pracę i wywołanie zwrotne (błąd), gdy skończysz, lub pominąć wywołanie zwrotne na liście argumentów i zostanie wywołane za Ciebie.

willTransitionFrom(transition, component, callback)

Wywoływane, gdy aktywna trasa jest przenoszona, co daje możliwość przerwania przejścia. Komponent jest składnikiem bieżącym, prawdopodobnie będziesz potrzebować go do sprawdzenia jego stanu, aby zdecydować, czy chcesz zezwolić na przejście (podobnie jak w przypadku pól formularza).

Przykład

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

    //...
  });

Do react-router1.0.0-RC1 z reactv0.14.x lub nowszej:

powinno to być możliwe dzięki routerWillLeavehaczykowi cyklu życia. W przypadku starszych wersji zobacz moją odpowiedź powyżej.

Z dokumentacji reaktora routera :

Aby zainstalować ten hak, użyj miksu Lifecycle w jednym ze składników trasy.

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

    // ...

  })

Rzeczy. może się jednak zmienić przed ostatecznym wydaniem.

Barry Staes
źródło
1

Możesz skorzystać z tego monitu.

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
źródło
1

Korzystanie z history.listen

Na przykład jak poniżej:

W Twoim komponencie

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Debabrata Ghosh
źródło
5
Cześć, witaj w Stack Overflow. Odpowiadając na pytanie, na które jest już wiele odpowiedzi, pamiętaj o dodaniu dodatkowego wglądu w to, dlaczego udzielona przez Ciebie odpowiedź jest merytoryczna, a nie po prostu odzwierciedlająca to, co zostało już sprawdzone przez oryginalny plakat. Jest to szczególnie ważne w przypadku odpowiedzi „tylko kod”, takich jak ta, którą podałeś.
chb