Rilevamento di utenti che lasciano la pagina con React-Router


117

Voglio che la mia app ReactJS notifichi un utente quando esci da una pagina specifica. Nello specifico un messaggio popup che gli ricorda di eseguire un'azione:

"Le modifiche vengono salvate, ma non ancora pubblicate. Farlo ora?"

Devo attivarlo a react-routerlivello globale o è qualcosa che può essere fatto dall'interno della pagina / componente di reazione?

Non ho trovato nulla su quest'ultimo, e preferirei evitare il primo. A meno che non sia la norma, ovviamente, ma questo mi fa chiedere come fare una cosa del genere senza dover aggiungere codice a ogni altra pagina possibile a cui l'utente può andare ..

Eventuali approfondimenti sono benvenuti, grazie!


3
Ora non so se questo è quello che stai cercando, ma puoi fare qc. in questo 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 } }modo puoi chiamare la tua funzione di pubblicazione bevor lasciando la pagina
Rico Berger,

Risposte:


154

react-routerv4 introduce un nuovo modo per bloccare la navigazione utilizzando Prompt. Basta aggiungerlo al componente che desideri bloccare:

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

Ciò bloccherà qualsiasi instradamento, ma non l'aggiornamento o la chiusura della pagina. Per bloccarlo, dovrai aggiungere questo (aggiornando secondo necessità con il ciclo di vita React appropriato):

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

onbeforeunload ha vari supporti dai browser.


9
Ciò si traduce in due avvisi dall'aspetto molto diverso.
XanderStrike

3
@XanderStrike Potresti provare a modellare l'avviso del prompt per imitare l'avviso predefinito del browser. Sfortunatamente, non c'è modo di definire lo stile onberforeunloaddell'avviso.
jcady

C'è un modo per mostrare lo stesso componente Prompt, ad esempio, una volta che l'utente fa clic su un pulsante ANNULLA?
Rene Enriquez

1
@ReneEnriquez react-routernon lo supporta immediatamente (supponendo che il pulsante Annulla non attivi alcun cambio di percorso). Tuttavia, puoi creare il tuo modale per imitare il comportamento.
jcady

6
Se finisci per usare onbeforeunload , ti consigliamo di ripulirlo quando il tuo componente si smonta. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch

40

In React-Router v2.4.0o superiore e prima v4ci sono diverse opzioni

  1. Aggiungi funzione onLeaveperRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Usa la funzione setRouteLeaveHookpercomponentDidMount

È possibile impedire che avvenga una transizione o avvisare l'utente prima di lasciare una rotta con un gancio di uscita.

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

    // ...

  })
)

Nota che questo esempio fa uso del withRoutercomponente di ordine superiore introdotto inv2.4.0.

Tuttavia, queste soluzioni non funzionano perfettamente quando si modifica manualmente il percorso nell'URL

Nel senso che

  • vediamo la Conferma - ok
  • il contenuto della pagina non si ricarica - ok
  • L'URL non cambia, non va bene

Per react-router v4utilizzare Prompt o cronologia personalizzata:

Tuttavia react-router v4, è piuttosto più facile da implementare con l'aiuto di Promptfrom'react-router

Secondo la documentazione

Richiesta

Utilizzato per chiedere all'utente prima di uscire da una pagina. Quando la tua applicazione entra in uno stato che dovrebbe impedire all'utente di allontanarsi (come un modulo è compilato a metà), esegui il rendering di un file <Prompt>.

import { Prompt } from 'react-router'

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

messaggio: stringa

Il messaggio con cui chiedere all'utente quando tenta di uscire.

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

messaggio: funz

Verrà chiamato con la posizione e l'azione successiva a cui l'utente sta tentando di navigare. Restituisce una stringa per mostrare un prompt all'utente o true per consentire la transizione.

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

quando: bool

Invece di <Prompt>eseguire il rendering condizionalmente di un dietro una guardia, puoi sempre renderlo ma passare when={true}o when={false}impedire o consentire la navigazione di conseguenza.

Nel tuo metodo di rendering devi semplicemente aggiungerlo come indicato nella documentazione in base alle tue necessità.

AGGIORNARE:

Nel caso in cui desideri avere un'azione personalizzata da intraprendere quando l'uso sta lasciando la pagina, puoi utilizzare la cronologia personalizzata e configurare il tuo router come

history.js

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

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

e poi nel tuo componente puoi usare history.blocklike

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

1
Anche se si utilizza <Prompt>l'URL viene modificato quando si preme Annulla nel prompt. Problema correlato: github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe

1
Vedo che questa domanda è ancora abbastanza popolare. Poiché le risposte migliori riguardano vecchie versioni deprecate, ho impostato questa risposta più recente come quella accettata. I collegamenti alla vecchia documentazione (e alle guide di aggiornamento) sono ora disponibili su github.com/ReactTraining/react-router
Barry Staes

1
onLeave viene attivato DOPO la conferma di un cambio di percorso. Puoi spiegarci come annullare una navigazione in onLeave?
schlingel

23

Per react-router2.4.0+

NOTA : è consigliabile migrare tutto il codice all'ultimo react-routerper ottenere tutti i nuovi gadget.

Come consigliato nella documentazione di react-router :

Si dovrebbe utilizzare il withRoutercomponente di ordine superiore:

Riteniamo che questo nuovo HoC sia più bello e più facile e lo utilizzeremo nella documentazione e negli esempi, ma non è un requisito difficile da cambiare.

Come esempio ES6 dalla documentazione:

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)

4
Che cosa fa il callback RouteLeaveHook? Richiede all'utente l'utilizzo di un modale integrato? E se volessi un modale personalizzato
Studente

Come @Learner: come farlo con una casella di conferma personalizzata come vex o SweetAlert o un'altra finestra di dialogo non bloccante?
olefrank

Sto riscontrando il seguente errore: - TypeError: Impossibile leggere la proprietà " id " di undefined. Qualsiasi suggerimento
Mustafa Mamun

@MustafaMamun Sembra un problema non correlato e per lo più vorrai creare una nuova domanda che spieghi i dettagli.
snymkpr

Ho riscontrato il problema mentre cercavo di utilizzare la soluzione. Il componente deve essere collegato direttamente al router, quindi questa soluzione funziona. Ho trovato una risposta migliore lì stackoverflow.com/questions/39103684/…
Mustafa Mamun

4

Per react-routerv3.x

Ho avuto lo stesso problema in cui avevo bisogno di un messaggio di conferma per qualsiasi modifica non salvata nella pagina. Nel mio caso, stavo usando React Router v3 , quindi non potevo usare <Prompt />, che è stato introdotto da React Router v4 .

Ho gestito il "clic del pulsante Indietro" e il "clic accidentale sul collegamento" con la combinazione di setRouteLeaveHooke history.pushState(), e ho gestito il "pulsante di ricarica" ​​con il onbeforeunloadgestore di eventi.

setRouteLeaveHook ( doc ) e history.pushState ( doc )

  • Utilizzare solo setRouteLeaveHook non era sufficiente. Per qualche motivo, l'URL è stato modificato sebbene la pagina sia rimasta la stessa quando si è fatto clic sul pulsante "Indietro".

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

  • Viene utilizzato per gestire il pulsante di "ricarica accidentale"

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

Di seguito è riportato il componente completo che ho scritto

  • nota che withRouter è usato per avere this.props.router.
  • nota che this.props.routeviene trasmesso dal componente chiamante
  • nota che currentStateviene passato come prop per avere lo stato iniziale e per controllare eventuali modifiche

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

Per favore fatemi sapere se c'è qualche domanda :)


da dove viene esattamente this.formStartEle?
Gaurav

2

Per react-routerv0.13.x con reactv0.13.x:

questo è possibile con i metodi willTransitionTo()e willTransitionFrom()statici. Per le versioni più recenti, vedi la mia altra risposta di seguito.

Dalla documentazione di react-router :

È possibile definire alcuni metodi statici sui gestori di route che verranno chiamati durante le transizioni di route.

willTransitionTo(transition, params, query, callback)

Chiamato quando un gestore sta per eseguire il rendering, dandoti l'opportunità di interrompere o reindirizzare la transizione. Puoi mettere in pausa la transizione mentre esegui un lavoro asincrono e chiamare callback (errore) quando hai finito, oppure omettere il callback nella tua lista di argomenti e verrà chiamato per te.

willTransitionFrom(transition, component, callback)

Chiamato quando è in corso la transizione di un percorso attivo, offrendoti l'opportunità di interrompere la transizione. Il componente è il componente corrente, probabilmente ti servirà per controllare il suo stato per decidere se vuoi consentire la transizione (come i campi del modulo).

Esempio

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

    //...
  });

Per react-router1.0.0-rc1 con reactv0.14.x o successiva:

questo dovrebbe essere possibile con il routerWillLeavelifecycle hook. Per le versioni precedenti, vedi la mia risposta sopra.

Dalla documentazione di react-router :

Per installare questo hook, utilizza il mixin del ciclo di vita in uno dei componenti del percorso.

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

    // ...

  })

Cose. potrebbe cambiare prima del rilascio finale.


1

Puoi usare questo prompt.

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

Utilizzando history.listen

Ad esempio come di seguito:

Nel tuo componente,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}

5
Ciao, benvenuto in Stack Overflow. Quando rispondi a una domanda che ha già molte risposte, assicurati di aggiungere ulteriori informazioni sul motivo per cui la risposta che stai fornendo è sostanziale e non semplicemente riecheggia ciò che è già stato controllato dal poster originale. Ciò è particolarmente importante nelle risposte "solo codice" come quella che hai fornito.
chb
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.