Erkennen, dass Benutzer die Seite mit dem React-Router verlassen

114

Ich möchte, dass meine ReactJS-App einen Benutzer benachrichtigt, wenn er von einer bestimmten Seite weg navigiert. Insbesondere eine Popup-Nachricht, die ihn / sie daran erinnert, eine Aktion auszuführen:

"Änderungen werden gespeichert, aber noch nicht veröffentlicht. Tun Sie das jetzt?"

Sollte ich dies react-routerglobal auslösen , oder kann dies innerhalb der Reaktionsseite / -komponente erfolgen?

Ich habe bei letzterem nichts gefunden, und ich möchte lieber das erste vermeiden. Es sei denn, es ist natürlich die Norm, aber das lässt mich fragen, wie man so etwas macht, ohne jeder anderen möglichen Seite, zu der der Benutzer gehen kann, Code hinzufügen zu müssen.

Irgendwelche Einblicke willkommen, danke!

Barry Staes
quelle
3
Ich weiß jetzt nicht, ob Sie danach suchen, aber Sie können etw tun. wie dies 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 } }so können Sie Ihre Funktion aufrufen veröffentlichen Bevor die Seite verlassen
Rico Berger

Antworten:

152

react-routerv4 führt eine neue Methode zum Blockieren der Navigation mit ein Prompt. Fügen Sie dies einfach zu der Komponente hinzu, die Sie blockieren möchten:

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

Dadurch wird jegliches Routing blockiert, jedoch keine Seitenaktualisierung oder -schließung. Um dies zu blockieren, müssen Sie Folgendes hinzufügen (Aktualisierung nach Bedarf mit dem entsprechenden React-Lebenszyklus):

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

onbeforeunload wird von Browsern unterschiedlich unterstützt.

jcady
quelle
9
Dies führt jedoch zu zwei sehr unterschiedlich aussehenden Warnungen.
XanderStrike
3
@XanderStrike Sie können versuchen, die Eingabeaufforderungswarnung so zu gestalten, dass sie die Standardwarnung des Browsers nachahmt. Leider gibt es keine Möglichkeit, die onberforeunloadWarnung zu formatieren.
Jcady
Gibt es eine Möglichkeit, dieselbe Eingabeaufforderungskomponente anzuzeigen, wenn der Benutzer beispielsweise auf eine Schaltfläche ABBRECHEN klickt?
Rene Enriquez
1
@ReneEnriquez unterstützt react-routerdies nicht sofort (vorausgesetzt, die Schaltfläche Abbrechen löst keine Routenänderungen aus). Sie können jedoch Ihr eigenes Modal erstellen, um das Verhalten nachzuahmen.
JCADY
5
Wenn Sie am Ende verwenden onbeforeunload , möchten Sie es bereinigen, wenn Ihre Komponente die Bereitstellung aufhebt. componentWillUnmount() { window.onbeforeunload = null; }
Deutsch
40

Im React-Router v2.4.0oder höher und davor v4gibt es mehrere Möglichkeiten

  1. Funktion hinzufügen onLeavefürRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Verwenden Sie die Funktion setRouteLeaveHookfürcomponentDidMount

Sie können verhindern, dass ein Übergang stattfindet, oder den Benutzer auffordern, bevor Sie eine Route mit einem Leave-Hook verlassen.

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

    // ...

  })
)

Beachten Sie, dass in diesem Beispiel die withRouterin eingeführte Komponente höherer Ordnung verwendet wirdv2.4.0.

Diese Lösung funktioniert jedoch nicht ganz perfekt, wenn die Route in der URL manuell geändert wird

In dem Sinne, dass

  • wir sehen die Bestätigung - ok
  • Seite enthalten wird nicht neu geladen - ok
  • URL ändert sich nicht - nicht in Ordnung

So react-router v4verwenden Sie die Eingabeaufforderung oder den benutzerdefinierten Verlauf:

In react-router v4ist es jedoch mit Hilfe des Promptfrom'react-Routers einfacher zu implementieren

Laut Dokumentation

Prompt

Wird verwendet, um den Benutzer aufzufordern, bevor er von einer Seite weg navigiert. Wenn Ihre Anwendung in einen Zustand wechselt, der den Benutzer daran hindern soll, weg zu navigieren (wie wenn ein Formular zur Hälfte ausgefüllt ist), rendern Sie a <Prompt>.

import { Prompt } from 'react-router'

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

Nachricht: Zeichenfolge

Die Nachricht, die den Benutzer auffordert, wenn er versucht, weg zu navigieren.

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

Nachricht: func

Wird mit dem nächsten Ort und der nächsten Aktion aufgerufen, zu der der Benutzer navigieren möchte. Geben Sie eine Zeichenfolge zurück, um dem Benutzer eine Eingabeaufforderung anzuzeigen, oder true, um den Übergang zuzulassen.

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

wann: bool

Stattdessen bedingt von einem Rendering <Prompt>hinter einer Wache, können Sie es jederzeit machen , aber passieren when={true}oder when={false}zur Navigation entsprechend zu verhindern oder zu ermöglichen.

In Ihrer Rendermethode müssen Sie dies einfach wie in der Dokumentation angegeben entsprechend Ihren Anforderungen hinzufügen.

AKTUALISIEREN:

Wenn Sie eine benutzerdefinierte Aktion ausführen möchten, wenn die Verwendung die Seite verlässt, können Sie den benutzerdefinierten Verlauf verwenden und Ihren Router wie folgt konfigurieren

history.js

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

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

und dann können Sie in Ihrer Komponente history.blocklike verwenden

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
quelle
1
Auch wenn Sie <Prompt>die URL verwenden, wird diese geändert, wenn Sie an der Eingabeaufforderung auf Abbrechen klicken. Verwandte Ausgabe: github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe
1
Ich sehe, dass diese Frage immer noch sehr beliebt ist. Da es sich bei den Top-Antworten um alte veraltete Versionen handelt, habe ich diese neuere Antwort als die akzeptierte festgelegt. Links zu alten Dokumentationen (und Upgrade-Anleitungen) sind jetzt unter github.com/ReactTraining/react-router
Barry Staes
1
onLeave wird ausgelöst, nachdem eine Routenänderung bestätigt wurde. Können Sie erläutern, wie Sie eine Navigation in onLeave abbrechen können?
Schlingel
23

Für react-router2.4.0+

HINWEIS : Es ist ratsam, Ihren gesamten Code auf den neuesten Stand zu migrieren react-router, um alle neuen Extras zu erhalten.

Wie in der React-Router-Dokumentation empfohlen :

Man sollte die withRouterKomponente höherer Ordnung verwenden:

Wir denken, dass dieser neue HoC schöner und einfacher ist und ihn in Dokumentationen und Beispielen verwenden wird, aber es ist keine schwierige Anforderung, ihn zu wechseln.

Als ES6-Beispiel aus der Dokumentation:

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
quelle
4
Was macht der RouteLeaveHook-Rückruf? Fordert es den Benutzer auf, ein eingebautes Modal zu verwenden? Was ist, wenn Sie ein benutzerdefiniertes Modal wollen
Learner
Gefällt mir @Learner: Wie geht das mit einem benutzerdefinierten Bestätigungsfeld wie vex oder SweetAlert oder einem anderen nicht blockierenden Dialogfeld?
Olefrank
Ich habe den folgenden Fehler: - TypeError: Die Eigenschaft ' id ' von undefined kann nicht gelesen werden . Jeder Vorschlag
Mustafa Mamun
@MustafaMamun Das scheint ein nicht verwandtes Problem zu sein, und Sie möchten meistens eine neue Frage erstellen, in der die Details erläutert werden.
Snymkpr
Ich hatte das Problem, als ich versuchte, die Lösung zu verwenden. Die Komponente muss direkt mit dem Router verbunden sein, damit diese Lösung funktioniert. Ich fand dort eine bessere Antwort stackoverflow.com/questions/39103684/…
Mustafa Mamun
4

Für react-routerv3.x.

Ich hatte das gleiche Problem, bei dem ich eine Bestätigungsnachricht für nicht gespeicherte Änderungen auf der Seite benötigte. In meinem Fall habe ich React Router v3 verwendet , daher konnte ich nicht verwenden <Prompt />, was von React Router v4 eingeführt wurde .

Ich behandeln ‚Zurück - Button klicken‘ und ‚zufälligen Link klicken‘ mit der Kombination aus setRouteLeaveHookund history.pushState()und behandeln ‚Reload - Button‘ mit onbeforeunloadEvent - Handler.

setRouteLeaveHook ( doc ) & history.pushState ( doc )

  • Es reichte nicht aus, nur setRouteLeaveHook zu verwenden. Aus irgendeinem Grund wurde die URL geändert, obwohl die Seite beim Klicken auf die Schaltfläche "Zurück" dieselbe blieb.

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

  • Es wird verwendet, um die Schaltfläche "Versehentliches Nachladen" zu behandeln

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

Unten ist die vollständige Komponente, die ich geschrieben habe

  • Beachten Sie, dass withRouter verwendet wird this.props.router.
  • Beachten Sie, dass this.props.routevon der aufrufenden Komponente weitergegeben wird
  • Beachten Sie, dass dies currentStateals Requisite übergeben wird, um den Anfangszustand zu haben und Änderungen zu überprüfen

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

Bitte lassen Sie mich wissen, wenn es irgendwelche Fragen gibt :)

Sang Yun Park
quelle
Wo genau kommt this.formStartEle her?
Gaurav
2

Für react-routerv0.13.x mit reactv0.13.x:

Dies ist mit den willTransitionTo()und willTransitionFrom()statischen Methoden möglich. Für neuere Versionen siehe meine andere Antwort unten.

Aus der React-Router-Dokumentation :

Sie können einige statische Methoden für Ihre Routenhandler definieren, die bei Routenübergängen aufgerufen werden.

willTransitionTo(transition, params, query, callback)

Wird aufgerufen, wenn ein Handler gerade gerendert wird, sodass Sie den Übergang abbrechen oder umleiten können. Sie können den Übergang anhalten, während Sie asynchrone Arbeit leisten und den Rückruf (Fehler) aufrufen, wenn Sie fertig sind, oder den Rückruf in Ihrer Argumentliste weglassen, und er wird für Sie aufgerufen.

willTransitionFrom(transition, component, callback)

Wird aufgerufen, wenn eine aktive Route umgestellt wird, sodass Sie die Möglichkeit haben, den Übergang abzubrechen. Die Komponente ist die aktuelle Komponente. Sie müssen sie wahrscheinlich überprüfen, um zu entscheiden, ob Sie den Übergang zulassen möchten (z. B. Formularfelder).

Beispiel

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

    //...
  });

Für react-router1.0.0-rc1 mit reactv0.14.x oder höher:

Dies sollte mit dem routerWillLeaveLifecycle-Hook möglich sein. Für ältere Versionen siehe meine Antwort oben.

Aus der React-Router-Dokumentation :

Verwenden Sie zum Installieren dieses Hooks das Lifecycle-Mixin in einer Ihrer Routenkomponenten.

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

    // ...

  })

Dinge. kann sich jedoch vor der endgültigen Veröffentlichung ändern.

Barry Staes
quelle
1

Sie können diese Eingabeaufforderung verwenden.

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
quelle
1

Verwenden von history.listen

Zum Beispiel wie unten:

In Ihrer Komponente,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Debabrata Ghosh
quelle
5
Hallo, willkommen bei Stack Overflow. Wenn Sie eine Frage beantworten, die bereits viele Antworten enthält, geben Sie bitte einen zusätzlichen Einblick, warum die von Ihnen bereitgestellte Antwort inhaltlich ist und nicht nur das wiedergibt, was bereits vom Originalplakat überprüft wurde. Dies ist besonders wichtig bei "Nur-Code" -Antworten wie der von Ihnen angegebenen.
chb