Détecter l'utilisateur quittant la page avec react-router

118

Je souhaite que mon application ReactJS informe un utilisateur lorsqu'il quitte une page spécifique. Plus précisément, un message contextuel qui lui rappelle de faire une action:

"Les modifications sont enregistrées, mais pas encore publiées. Faites-le maintenant?"

Dois-je déclencher cela react-routerglobalement, ou est-ce quelque chose qui peut être fait à partir de la page / composant de réaction?

Je n'ai rien trouvé sur ce dernier et je préfère éviter le premier. À moins que ce ne soit la norme, bien sûr, mais cela me fait me demander comment faire une telle chose sans avoir à ajouter du code à toutes les autres pages possibles auxquelles l'utilisateur peut accéder.

Toutes les informations sont les bienvenues, merci!

Barry Staes
la source
3
Je ne sais pas maintenant si c'est ce que vous recherchez, mais vous pouvez faire qc. comme ça 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 } }pour que vous puissiez appeler votre fonction de publication avant de quitter la page
Rico Berger

Réponses:

154

react-routerv4 introduit une nouvelle façon de bloquer la navigation à l'aide de Prompt. Ajoutez simplement ceci au composant que vous souhaitez bloquer:

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

Cela bloquera tout routage, mais pas l'actualisation ou la fermeture de la page. Pour bloquer cela, vous devrez ajouter ceci (mise à jour si nécessaire avec le cycle de vie React approprié):

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

onbeforeunload a divers supports par les navigateurs.

jcady
la source
9
Cela se traduit cependant par deux alertes très différentes.
XanderStrike
3
@XanderStrike Vous pouvez essayer de styliser l'alerte d'invite pour imiter l'alerte par défaut du navigateur. Malheureusement, il n'y a aucun moyen de styliser l' onberforeunloadalerte.
jcady
Existe-t-il un moyen d'afficher le même composant Prompt une fois que l'utilisateur clique sur un bouton ANNULER par exemple?
Rene Enriquez
1
@ReneEnriquez react-routerne prend pas en charge cela par défaut (en supposant que le bouton d'annulation ne déclenche aucun changement d'itinéraire). Vous pouvez cependant créer votre propre modal pour imiter le comportement.
jcady
6
Si vous onbeforeunload finissez par utiliser , vous voudrez le nettoyer lorsque votre composant se démonte. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch
40

Dans react-router v2.4.0ou au-dessus et avant v4il y a plusieurs options

  1. Ajouter une fonction onLeavepourRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Utiliser la fonction setRouteLeaveHookpourcomponentDidMount

Vous pouvez empêcher une transition de se produire ou demander à l'utilisateur de quitter une route avec un crochet de sortie.

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

    // ...

  })
)

Notez que cet exemple utilise le withRoutercomposant d'ordre supérieur introduit dansv2.4.0.

Cependant, cette solution ne fonctionne pas parfaitement lors du changement manuel de l'itinéraire dans l'URL

Dans le sens où

  • nous voyons la confirmation - ok
  • le contenu de la page ne se recharge pas - ok
  • L'URL ne change pas - pas d'accord

Pour react-router v4utiliser l'invite ou l'historique personnalisé:

Cependant react-router v4, c'est plutôt plus facile à implémenter avec l'aide de Promptfrom'react-router

Selon la documentation

Rapide

Utilisé pour inviter l'utilisateur avant de quitter une page. Lorsque votre application entre dans un état qui devrait empêcher l'utilisateur de s'éloigner (comme un formulaire à moitié rempli), affichez un fichier <Prompt>.

import { Prompt } from 'react-router'

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

message: chaîne

Message à inviter l'utilisateur lorsqu'il essaie de s'éloigner.

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

message: func

Sera appelé avec le prochain emplacement et l'action vers lesquels l'utilisateur tente de naviguer. Renvoie une chaîne pour afficher une invite à l'utilisateur ou true pour autoriser la transition.

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

quand: booléen

Au lieu de rendre conditionnellement un <Prompt>derrière une garde, vous pouvez toujours le rendre mais passer when={true}ou when={false}empêcher ou autoriser la navigation en conséquence.

Dans votre méthode de rendu, vous devez simplement l'ajouter comme indiqué dans la documentation en fonction de vos besoins.

METTRE À JOUR:

Au cas où vous souhaiteriez avoir une action personnalisée à entreprendre lorsque l'utilisation quitte la page, vous pouvez utiliser l'historique personnalisé et configurer votre routeur comme

history.js

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

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

puis dans votre composant, vous pouvez utiliser history.blockcomme

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
la source
1
Même si vous utilisez <Prompt>l'URL est modifiée lorsque vous appuyez sur Annuler à l'invite. Problème
Sahan Serasinghe
1
Je vois que cette question est toujours très populaire. Étant donné que les principales réponses concernent les anciennes versions obsolètes, j'ai défini cette nouvelle réponse comme étant acceptée. Les liens vers l'ancienne documentation (et les guides de mise à niveau) sont maintenant disponibles à l'adresse github.com/ReactTraining/react-router
Barry Staes
1
onLeave est déclenché APRÈS la confirmation d'un changement d'itinéraire. Pouvez-vous expliquer comment annuler une navigation dans onLeave?
schlingel
23

Pour react-router2.4.0+

REMARQUE : il est conseillé de migrer tout votre code vers le dernier react-routerpour obtenir tous les nouveaux goodies.

Comme recommandé dans la documentation de react-router :

Il faut utiliser le withRoutercomposant d'ordre supérieur:

Nous pensons que ce nouveau HoC est plus agréable et plus facile, et nous l'utilisons dans la documentation et les exemples, mais ce n'est pas une exigence difficile à changer.

Comme exemple ES6 de la documentation:

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
la source
4
Que fait le rappel RouteLeaveHook? Invite-t-il l'utilisateur à utiliser un modal intégré? Et si vous voulez un modal personnalisé
Apprenant
Comme @Learner: Comment faire cela avec une boîte de confirmation personnalisée comme vex ou SweetAlert ou une autre boîte de dialogue non bloquante?
olefrank
J'ai l'erreur suivante: - TypeError: Impossible de lire la propriété ' id ' de undefined. Toute suggestion
Mustafa Mamun
@MustafaMamun Cela semble être un problème sans rapport et vous voudrez surtout créer une nouvelle question expliquant les détails.
snymkpr
J'avais le problème en essayant d'utiliser la solution. Le composant doit être directement connecté au routeur puis cette solution fonctionne. J'ai trouvé une meilleure réponse là-bas stackoverflow.com/questions/39103684/…
Mustafa Mamun
4

Pour react-routerv3.x

J'ai eu le même problème où j'avais besoin d'un message de confirmation pour toute modification non enregistrée sur la page. Dans mon cas, j'utilisais React Router v3 , donc je ne pouvais pas utiliser <Prompt />, qui a été introduit à partir de React Router v4 .

J'ai géré le «clic sur le bouton retour» et le «clic sur un lien accidentel» avec la combinaison de setRouteLeaveHooket history.pushState(), et j'ai géré le «bouton de rechargement» avec le onbeforeunloadgestionnaire d'événements.

setRouteLeaveHook ( doc ) et history.pushState ( doc )

  • Utiliser uniquement setRouteLeaveHook ne suffisait pas. Pour une raison quelconque, l'URL a été modifiée bien que la page soit restée la même lorsque l'utilisateur a cliqué sur le bouton de retour.

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

  • Il est utilisé pour gérer le bouton de `` rechargement accidentel ''

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

Voici le composant complet que j'ai écrit

  • notez que withRouter est utilisé pour avoirthis.props.router .
  • notez qui this.props.routeest transmis par le composant appelant
  • notez qu'il currentStateest passé comme accessoire pour avoir l'état initial et pour vérifier tout changement

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

S'il vous plaît laissez-moi savoir s'il y a des questions :)

Parc Sang Yun
la source
d'où vient exactement this.formStartEle?
Gaurav
2

Pour react-routerv0.13.x avec reactv0.13.x:

cela est possible avec les méthodes willTransitionTo()et willTransitionFrom()static. Pour les versions plus récentes, voir mon autre réponse ci-dessous.

À partir de la documentation de react-router :

Vous pouvez définir des méthodes statiques sur vos gestionnaires d'itinéraire qui seront appelées pendant les transitions d'itinéraire.

willTransitionTo(transition, params, query, callback)

Appelé lorsqu'un gestionnaire est sur le point d'effectuer le rendu, vous offrant la possibilité d'abandonner ou de rediriger la transition. Vous pouvez suspendre la transition pendant que vous effectuez un travail asynchrone et appeler le rappel (erreur) lorsque vous avez terminé, ou omettre le rappel dans votre liste d'arguments et il sera appelé pour vous.

willTransitionFrom(transition, component, callback)

Appelé lorsqu'une route active est en cours de transition, ce qui vous donne la possibilité d'interrompre la transition. Le composant est le composant actuel, vous en aurez probablement besoin pour vérifier son état pour décider si vous souhaitez autoriser la transition (comme les champs de formulaire).

Exemple

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

    //...
  });

Pour react-router1.0.0-rc1 avec reactv0.14.x ou version ultérieure:

cela devrait être possible avec le routerWillLeavehook de cycle de vie. Pour les anciennes versions, voir ma réponse ci-dessus.

À partir de la documentation de react-router :

Pour installer ce hook, utilisez le mixin Lifecycle dans l'un de vos composants d'itinéraire.

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

    // ...

  })

Des choses. peut changer avant la version finale.

Barry Staes
la source
1

Vous pouvez utiliser cette invite.

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
la source
1

Utilisation de history.listen

Par exemple comme ci-dessous:

Dans votre composant,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Debabrata Ghosh
la source
5
Bonjour, bienvenue dans Stack Overflow. Lorsque vous répondez à une question qui a déjà de nombreuses réponses, assurez-vous d'ajouter des informations supplémentaires sur les raisons pour lesquelles la réponse que vous fournissez est substantielle et ne fait pas simplement écho à ce qui a déjà été vérifié par l'affiche originale. Ceci est particulièrement important dans les réponses «code uniquement» telles que celle que vous avez fournie.
chb