Detectando usuario saliendo de la página con react-router

115

Quiero que mi aplicación ReactJS notifique a un usuario cuando navega fuera de una página específica. Específicamente, un mensaje emergente que le recuerda que debe realizar una acción:

"Los cambios se guardan, pero aún no se publican. ¿Hacer eso ahora?"

¿Debo activar esto react-routerglobalmente, o es algo que se puede hacer desde la página / componente de reacción?

No he encontrado nada sobre este último y prefiero evitar el primero. A menos que sea la norma, por supuesto, pero eso me hace preguntarme cómo hacer tal cosa sin tener que agregar código a todas las demás páginas posibles a las que el usuario puede ir.

Cualquier información es bienvenida, ¡gracias!

Barry Staes
fuente
3
No sé si esto es lo que estás buscando, pero puedes hacer algo. así 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 } }para que pueda llamar a su función de publicación antes de salir de la página
Rico Berger

Respuestas:

153

react-routerv4 introduce una nueva forma de bloquear la navegación usando Prompt. Simplemente agregue esto al componente que le gustaría bloquear:

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

Esto bloqueará cualquier enrutamiento, pero no la actualización ni el cierre de la página. Para bloquear eso, deberá agregar esto (actualizando según sea necesario con el ciclo de vida apropiado de React):

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

onbeforeunload tiene varios tipos de compatibilidad con los navegadores.

jcady
fuente
9
Sin embargo, esto da como resultado dos alertas de aspecto muy diferente.
XanderStrike
3
@XanderStrike Puede intentar diseñar la alerta de aviso para imitar la alerta predeterminada del navegador. Desafortunadamente, no hay forma de diseñar la onberforeunloadalerta.
jcady
¿Hay alguna forma de mostrar el mismo componente de aviso una vez que el usuario hace clic en un botón CANCELAR, por ejemplo?
Rene Enriquez
1
@ReneEnriquez react-routerno admite eso de inmediato (asumiendo que el botón cancelar no activa ningún cambio de ruta). Sin embargo, puede crear su propio modal para imitar el comportamiento.
jcady
6
Si termina usándolo onbeforeunload , querrá limpiarlo cuando se desmonte su componente. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch
40

En react-router v2.4.0o superior y antes v4hay varias opciones

  1. Agregar función onLeaveparaRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Usar función setRouteLeaveHookparacomponentDidMount

Puede evitar que se produzca una transición o avisar al usuario antes de abandonar una ruta con un gancho de salida.

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

    // ...

  })
)

Tenga en cuenta que este ejemplo hace uso del withRoutercomponente de orden superior introducido env2.4.0.

Sin embargo, esta solución no funciona a la perfección cuando se cambia la ruta en la URL manualmente

En el sentido de que

  • vemos la Confirmación - ok
  • el contenido de la página no se recarga - ok
  • La URL no cambia, no está bien

Para react-router v4usar el historial personalizado o Prompt:

Sin embargo react-router v4, es bastante más fácil de implementar con la ayuda de Promptfrom'react-router

Según la documentación

Rápido

Se utiliza para avisar al usuario antes de salir de una página. Cuando su aplicación entra en un estado que debería evitar que el usuario navegue (como si un formulario estuviera medio lleno), renderice un archivo <Prompt>.

import { Prompt } from 'react-router'

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

mensaje: cadena

El mensaje para avisar al usuario cuando intenta salir.

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

mensaje: func

Se llamará con la siguiente ubicación y acción a la que el usuario está intentando navegar. Devuelve una cadena para mostrar un mensaje al usuario o true para permitir la transición.

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

cuando: bool

En lugar de renderizar condicionalmente un <Prompt>detrás de un guardia, siempre puede renderizarlo pero pasar when={true}o when={false}para prevenir o permitir la navegación en consecuencia.

En su método de renderizado, simplemente necesita agregar esto como se menciona en la documentación de acuerdo con sus necesidades.

ACTUALIZAR:

En caso de que desee tener una acción personalizada para realizar cuando el uso está saliendo de la página, puede hacer uso del historial personalizado y configurar su enrutador como

history.js

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

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

y luego en su componente puede hacer uso de 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
   }
}
Shubham Khatri
fuente
1
Incluso si usa <Prompt>la URL se cambia cuando presiona Cancelar en el mensaje. Problema
Sahan Serasinghe
1
Veo que esta pregunta sigue siendo bastante popular. Dado que las respuestas principales son para versiones antiguas obsoletas, configuré esta respuesta más nueva como la aceptada. Los enlaces a la documentación anterior (y las guías de actualización) ahora están disponibles en github.com/ReactTraining/react-router
Barry Staes
1
onLeave se activa DESPUÉS de que se confirme un cambio de ruta. ¿Puede explicarnos cómo cancelar una navegación en onLeave?
schlingel
23

Para react-router2.4.0+

NOTA : Es recomendable migrar todo su código a la última versión react-routerpara obtener todas las novedades.

Como se recomienda en la documentación de react-router :

Uno debe usar el withRoutercomponente de orden superior:

Creemos que este nuevo HoC es más agradable y más fácil, y lo usaremos en documentación y ejemplos, pero no es un requisito difícil cambiar.

Como ejemplo de ES6 de la documentación:

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
fuente
4
¿Qué hace la devolución de llamada RouteLeaveHook? ¿Le pide al usuario que utilice un modal incorporado? ¿Qué pasa si quieres un modal personalizado?
Learner
Me gusta @Learner: ¿Cómo hacer esto con un cuadro de confirmación personalizado como vex o SweetAlert u otro cuadro de diálogo sin bloqueo?
olefrank
Tengo el siguiente error: - TypeError: No se puede leer la propiedad ' id ' de undefined. Cualquier sugerencia
Mustafa Mamun
@MustafaMamun Eso parece un problema no relacionado y en su mayoría querrá crear una nueva pregunta explicando los detalles.
snymkpr
Tenía el problema al intentar usar la solución. El componente tiene que estar conectado directamente al enrutador, entonces esta solución funciona. Encontré una mejor respuesta allí stackoverflow.com/questions/39103684/…
Mustafa Mamun
4

Para react-routerv3.x

Tuve el mismo problema en el que necesitaba un mensaje de confirmación para cualquier cambio no guardado en la página. En mi caso, estaba usando React Router v3 , por lo que no pude usar <Prompt />, que se introdujo desde React Router v4 .

Manejé 'hacer clic en el botón de retroceso' y 'clic accidental en un enlace' con la combinación de setRouteLeaveHooky history.pushState(), y manejé 'botón de recarga' con el onbeforeunloadcontrolador de eventos.

setRouteLeaveHook ( doc ) e history.pushState ( doc )

  • Usar solo setRouteLeaveHook no fue suficiente. Por alguna razón, se cambió la URL, aunque la página permaneció igual cuando se hizo clic en el botón "Atrás".

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

  • Se utiliza para manejar el botón de 'recarga accidental'

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

A continuación se muestra el componente completo que he escrito

  • tenga en cuenta que withRouter se suele tener this.props.router.
  • tenga en cuenta que this.props.routese transmite desde el componente de llamada
  • tenga en cuenta que currentStatese pasa como prop para tener un estado inicial y para verificar cualquier cambio

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

Por favor, avíseme si tiene alguna pregunta :)

Parque Sang Yun
fuente
¿De dónde viene exactamente this.formStartEle?
Gaurav
2

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

esto es posible con los métodos estáticos willTransitionTo()y willTransitionFrom(). Para versiones más nuevas, vea mi otra respuesta a continuación.

De la documentación de react-router :

Puede definir algunos métodos estáticos en sus controladores de ruta que serán llamados durante las transiciones de ruta.

willTransitionTo(transition, params, query, callback)

Se llama cuando un controlador está a punto de renderizarse, lo que le brinda la oportunidad de abortar o redirigir la transición. Puede pausar la transición mientras realiza un trabajo asincrónico y llamar a la devolución de llamada (error) cuando haya terminado, u omitir la devolución de llamada en su lista de argumentos y se llamará por usted.

willTransitionFrom(transition, component, callback)

Se llama cuando se realiza la transición de una ruta activa, lo que le brinda la oportunidad de abortar la transición. El componente es el componente actual, probablemente lo necesitará para verificar su estado y decidir si desea permitir la transición (como campos de formulario).

Ejemplo

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

    //...
  });

Para react-router1.0.0-rc1 con reactv0.14.xo posterior:

esto debería ser posible con el routerWillLeaveenlace del ciclo de vida. Para versiones anteriores, vea mi respuesta anterior.

De la documentación de react-router :

Para instalar este gancho, use la combinación de ciclo de vida en uno de los componentes de su ruta.

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

    // ...

  })

Cosas. Sin embargo, puede cambiar antes del lanzamiento final.

Barry Staes
fuente
1

Puede utilizar este mensaje.

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

Usando history.listen

Por ejemplo, como a continuación:

En su componente,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Debabrata Ghosh
fuente
5
Hola, bienvenido a Stack Overflow. Al responder una pregunta que ya tiene muchas respuestas, asegúrese de agregar información adicional sobre por qué la respuesta que está brindando es sustantiva y no simplemente se hace eco de lo que ya ha sido examinado por el póster original. Esto es especialmente importante en respuestas de "solo código" como la que proporcionó.
chb