Detecção de usuário saindo da página com react-roteador

117

Quero que meu aplicativo ReactJS notifique um usuário ao navegar para fora de uma página específica. Especificamente, uma mensagem pop-up que o lembra de fazer uma ação:

"As alterações foram salvas, mas ainda não foram publicadas. Quer fazer isso agora?"

Devo acionar isso react-routerglobalmente ou isso é algo que pode ser feito na página / componente de reação?

Não encontrei nada sobre o último e prefiro evitar o primeiro. A menos que seja a norma, é claro, mas isso me faz pensar em como fazer isso sem ter que adicionar código a todas as outras páginas possíveis que o usuário pode acessar.

Quaisquer insights são bem-vindos, obrigado!

Barry Staes
fonte
3
Não sei se é isso que você está procurando, mas você pode fazer tudo. assim, 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 você possa chamar sua função de publicação antes de sair da página
Rico Berger

Respostas:

154

react-routerv4 apresenta uma nova maneira de bloquear a navegação usando Prompt. Basta adicionar ao componente que você deseja 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>
)

Isso bloqueará qualquer roteamento, mas não a atualização ou o fechamento da página. Para bloquear isso, você precisará adicionar isto (atualizando conforme necessário com o ciclo de vida React apropriado):

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

onbeforeunload tem vários tipos de suporte por navegadores.

jcady
fonte
9
Isso resulta em dois alertas de aparência muito diferentes.
XanderStrike
3
@XanderStrike Você pode tentar estilizar o alerta de prompt para imitar o alerta padrão do navegador. Infelizmente, não há como definir o estilo do onberforeunloadalerta.
jcady
Existe alguma maneira de mostrar o mesmo componente Prompt depois que o usuário clica em um botão CANCELAR, por exemplo?
Rene Enriquez,
1
@ReneEnriquez react-routernão oferece suporte para isso fora da caixa (assumindo que o botão Cancelar não acione nenhuma alteração de rota). Você pode criar seu próprio modal para simular o comportamento.
jcady,
6
Se você acabar usando onbeforeunload , você vai querer limpá-lo quando o componente for desmontado. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch de
40

No react-router v2.4.0ou superior e antes, v4existem várias opções

  1. Adicionar função onLeaveparaRoute
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Use a função setRouteLeaveHookparacomponentDidMount

Você pode impedir que uma transição aconteça ou avisar o usuário antes de sair de uma rota com um gancho de saída.

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

    // ...

  })
)

Observe que este exemplo faz uso do withRouter componente de ordem superior introduzido emv2.4.0.

No entanto, essa solução não funciona perfeitamente ao alterar a rota no URL manualmente

No sentido de que

  • vemos a confirmação - ok
  • contém da página não recarrega - ok
  • URL não muda - não está bem

Para react-router v4usar Prompt ou histórico personalizado:

No entanto react-router v4, é bastante mais fácil de implementar com a ajuda de Promptfrom'react-router

De acordo com a documentação

Pronto

Usado para avisar o usuário antes de navegar para fora de uma página. Quando seu aplicativo entra em um estado que deve impedir o usuário de navegar (como se um formulário estivesse parcialmente preenchido), renderize a <Prompt>.

import { Prompt } from 'react-router'

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

mensagem: string

A mensagem a ser avisada ao usuário quando ele tenta navegar para fora.

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

mensagem: func

Será chamado com o próximo local e ação para a qual o usuário está tentando navegar. Retorne uma string para mostrar um prompt ao usuário ou true para permitir a transição.

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

quando: bool

Em vez de renderizar condicionalmente um <Prompt>atrás de um guarda, você sempre pode renderizá-lo, mas passar when={true}ou when={false}impedir ou permitir a navegação de acordo.

Em seu método de renderização, você simplesmente precisa adicionar isso conforme mencionado na documentação de acordo com sua necessidade.

ATUALIZAR:

No caso de desejar ter uma ação personalizada a ser executada quando o uso estiver saindo da página, você pode usar o histórico personalizado e configurar seu roteador como

history.js

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

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

e, em seguida, na sua componente que você pode fazer uso de history.blockcomo

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
fonte
1
Mesmo se você usar, <Prompt>o URL é alterado quando você pressiona Cancelar no prompt. Problema relacionado: github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe
1
Vejo que essa pergunta ainda é bastante popular. Uma vez que as principais respostas referem-se a versões antigas obsoletas, defini esta resposta mais recente como a aceita. Links para documentação antiga (e guias de atualização) agora estão disponíveis em github.com/ReactTraining/react-router
Barry Staes
1
onLeave é disparado APÓS a confirmação de uma mudança de rota. Você pode explicar como cancelar uma navegação no onLeave?
Schlingel
23

Para react-router2.4.0+

NOTA : É aconselhável migrar todo o seu código para o mais recente react-routerpara obter todas as novidades.

Conforme recomendado na documentação do roteador react :

Deve-se usar o withRoutercomponente de ordem superior:

Achamos que este novo HoC é mais agradável e fácil, e o usaremos em documentação e exemplos, mas não é um requisito difícil de trocar.

Como um exemplo ES6 da documentação:

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
fonte
4
O que o retorno de chamada RouteLeaveHook faz? Ele avisa o usuário usando um modal integrado? E se você quiser um modal personalizado
Aluno,
Como @Learner: como fazer isso com uma caixa de confirmação personalizada como vex ou SweetAlert ou outra caixa de diálogo sem bloqueio?
Olefrank
Estou tendo o seguinte erro: - TypeError: Não é possível ler a propriedade ' id ' de undefined. Qualquer sugestão
Mustafa Mamun
@MustafaMamun Esse parece um problema não relacionado e você desejará principalmente criar uma nova pergunta explicando os detalhes.
snymkpr
Eu estava tendo o problema ao tentar usar a solução. O componente deve ser conectado diretamente ao roteador para que esta solução funcione. Encontrei uma resposta melhor lá stackoverflow.com/questions/39103684/…
Mustafa Mamun
4

Para react-routerv3.x

Eu tive o mesmo problema em que precisava de uma mensagem de confirmação para qualquer alteração não salva na página. No meu caso, eu estava usando o React Router v3 , então não pude usar <Prompt />, que foi apresentado a partir do React Router v4 .

Eu lidei com 'clique no botão voltar' e 'clique acidental no link' com a combinação de setRouteLeaveHooke history.pushState(), e lidei com 'botão recarregar' com o onbeforeunloadmanipulador de eventos.

setRouteLeaveHook ( doc ) & history.pushState ( doc )

  • Usar apenas setRouteLeaveHook não era suficiente. Por algum motivo, o URL foi alterado, embora a página permanecesse a mesma quando o 'botão Voltar' foi clicado.

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

  • É usado para lidar com o botão de 'recarga acidental'

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

Abaixo está o componente completo que escrevi

  • observe que withRouter costumava ter this.props.router.
  • observe que this.props.routeé transmitido do componente de chamada
  • note que currentStateé passado como prop para ter estado inicial e para verificar qualquer mudança

    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, deixe-me saber se houver alguma dúvida :)

Parque Sang Yun
fonte
de onde exatamente está vindo this.formStartEle?
Gaurav
2

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

isso é possível com os métodos estáticos willTransitionTo()e willTransitionFrom(). Para versões mais recentes, veja minha outra resposta abaixo.

Da documentação do react-router :

Você pode definir alguns métodos estáticos em seus manipuladores de rota que serão chamados durante as transições de rota.

willTransitionTo(transition, params, query, callback)

Chamado quando um manipulador está prestes a renderizar, dando a você a oportunidade de abortar ou redirecionar a transição. Você pode pausar a transição enquanto faz algum trabalho assíncrono e chamar o retorno de chamada (erro) quando terminar, ou omitir o retorno de chamada em sua lista de argumentos e ele será chamado para você.

willTransitionFrom(transition, component, callback)

Chamado quando uma rota ativa está em transição, dando a você a oportunidade de abortar a transição. O componente é o componente atual, você provavelmente precisará verificar seu estado para decidir se deseja permitir a transição (como campos de formulário).

Exemplo

  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 com reactv0.14.x ou posterior:

isso deve ser possível com o routerWillLeavegancho do ciclo de vida. Para versões mais antigas, veja minha resposta acima.

Da documentação do react-router :

Para instalar este gancho, use o mixin Lifecycle em um de seus componentes de rota.

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

    // ...

  })

Coisas. pode mudar antes do lançamento final.

Barry Staes
fonte
1

Você pode usar este 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;
Jaskaran Singh
fonte
1

Usando history.listen

Por exemplo, como abaixo:

Em seu componente,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Debabrata Ghosh
fonte
5
Olá, bem-vindo ao Stack Overflow. Ao responder a uma pergunta que já tem muitas respostas, certifique-se de adicionar alguns insights adicionais sobre por que a resposta que você está fornecendo é substantiva e não apenas ecoando o que já foi examinado pelo autor original. Isso é especialmente importante em respostas "somente código", como a que você forneceu.
chb