web-dev-qa-db-fra.com

Détecter un utilisateur quittant la page avec react-router

Je souhaite que mon application ReactJS en informe un utilisateur lors de la navigation en dehors d'une page spécifique. Plus précisément, un message contextuel lui rappelant de faire une action:

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

Dois-je déclencher ceci sur react-router globalement, ou est-ce quelque chose qui peut être fait à partir de la page/du 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 je me demande comment faire une telle chose sans avoir à ajouter du code à toutes les autres pages possibles auxquelles l'utilisateur peut accéder.

Toute idée bienvenue, merci!

64
Barry Staes

react-router v4 introduit un nouveau moyen de bloquer la navigation en utilisant 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 devez ajouter ceci (en mettant à jour si nécessaire avec le cycle de vie approprié React)):

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

onbeforeunload supporte divers navigateurs.

72
jcady

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

  1. Ajout de la fonction onLeave pour Route
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Utilisez la fonction setRouteLeaveHook pour componentDidMount

Vous pouvez empêcher une transition ou demander à l'utilisateur avant de quitter une route avec un crochet de congé.

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 composant d'ordre supérieur withRouter introduit dans v2.4.0.

Cependant, ces solutions ne fonctionnent pas parfaitement lorsque vous modifiez manuellement l'itinéraire dans une URL.

Dans le sens où

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

Pour react-router v4 utilisant l’invite ou l’historique personnalisé:

Cependant dans react-router v4, c’est plutôt plus facile à mettre en oeuvre à l’aide de Prompt from'react-router

Selon la documentation

Invite

Utilisé pour inviter l'utilisateur avant de quitter une page. Lorsque votre application entre dans un état empêchant l'utilisateur de naviguer (comme si un formulaire est à moitié rempli), restituez un <Prompt>.

import { Prompt } from 'react-router'

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

message: chaîne de caractères

Le message à Demander à l'utilisateur lorsqu'il tente de s'éloigner.

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

message: func

Sera appelé avec le prochain emplacement et l'action que 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

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

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

UPDATE:

Dans le cas où vous souhaiteriez avoir une action personnalisée à effectuer lorsque l’utilisation quitte la page, vous pouvez utiliser l’historique personnalisé et configurer votre routeur de la manière suivante:

history.js

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

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

et puis dans votre composant, vous pouvez utiliser history.block comme

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
   }
}
29
Shubham Khatri

Pour react-router 2.4.0 +

[~ # ~] note [~ # ~] : Il est conseillé de migrer tout votre code au plus récent react-router pour obtenir tous les nouveaux goodies.

Comme recommandé dans le documentation de react-router :

On devrait utiliser le composant d'ordre supérieur withRouter:

Nous pensons que cette nouvelle CdC est plus agréable et plus simple et qu’elle l’utilisera dans la documentation et les exemples, mais il n’est pas difficile de changer de fournisseur.

À titre d'exemple ES6 tiré 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)
23
activatedgeek

Pour react-router V3.x

J'ai eu le même problème pour lequel 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 , je ne pouvais donc pas utiliser <Prompt />, Qui avait été introduit à partir de React Router v4 .

J'ai manipulé le 'clic du bouton de retour' et le 'clic du lien accidentel' avec la combinaison de setRouteLeaveHook et de history.pushState(), et le 'bouton de rechargement' avec le gestionnaire d'événements onbeforeunload.

setRouteLeaveHook ( doc ) & history.pushState ( doc )

  • Utiliser uniquement setRouteLeaveHook n'était pas suffisant. Pour une raison quelconque, l'URL a été modifiée même si la page est restée la même lorsque le bouton "Précédent" a été cliqué.

      // 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;
    }
    

Ci-dessous le composant complet que j'ai écrit

  • notez que withRouter est utilisé pour avoir this.props.router.
  • notez que this.props.route est transmis du composant appelant
  • notez que currentState est passé en tant que prop 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 une question :)

4
Sang Yun Park

Pour react-router V0.13.x avec react v0.13.x:

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

De la documentation de react-router :

Vous pouvez définir des méthodes statiques sur vos gestionnaires de routage qui seront appelées lors des transitions de routage.

willTransitionTo(transition, params, query, callback)

Appelé lorsqu'un gestionnaire est sur le point de rendre, ce qui vous permet d'abandonner ou de rediriger la transition. Vous pouvez suspendre la transition pendant que vous effectuez un travail asynchrone et appeler un 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é lors de la transition d'un itinéraire actif en sortie, ce qui vous permet d'interrompre la transition. Le composant est le composant actuel, vous en aurez probablement besoin pour vérifier son état afin de 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-router 1.0.0-rc1 avec react v0.14.x ou version ultérieure:

cela devrait être possible avec le hook routerWillLeave lifecycle. Pour les versions plus anciennes, voir ma réponse ci-dessus.

De la documentation de react-router :

Pour installer ce hook, utilisez le mixin Lifecycle dans l’un de vos composants de routage.

  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 cependant.

2
Barry Staes

tilisation 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);
    });
  }
1
Debabrata Ghosh

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